From bc5470d05ab1db819783ddc55a093048a4c9b1a5 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Fri, 7 Aug 2020 11:05:41 -0400 Subject: [PATCH] Add ViewTree, onHover modifier, and various other debug changes --- Package.resolved | 9 +++ Package.swift | 9 ++- .../TokamakCore/Modifiers/ViewModifier.swift | 18 ++++- .../Modifiers/_HoverRegionModifier.swift | 33 ++++++++++ .../TokamakCore/MountedViews/MountedApp.swift | 21 +++++- .../MountedViews/MountedCompositeView.swift | 21 +++++- .../MountedViews/MountedElement.swift | 6 +- .../MountedViews/MountedEmptyView.swift | 8 +++ .../MountedViews/MountedHostView.swift | 23 ++++++- .../MountedViews/MountedScene.swift | 21 +++++- Sources/TokamakCore/Renderer.swift | 10 +++ Sources/TokamakCore/StackReconciler.swift | 31 +++++++++ Sources/TokamakCore/Target.swift | 6 ++ Sources/TokamakCore/ViewTree.swift | 66 +++++++++++++++++++ Sources/TokamakDOM/DOMNode.swift | 7 ++ Sources/TokamakDOM/DOMRenderer.swift | 22 +++++++ .../Modifiers/_HoverRegionModifier.swift | 27 ++++++++ Sources/TokamakDemo/HoverDemo.swift | 18 +++++ Sources/TokamakDemo/TokamakDemo.swift | 3 + .../Modifiers/ModifiedContent.swift | 2 + .../Resources/TokamakStyles.swift | 16 +++++ 21 files changed, 368 insertions(+), 9 deletions(-) create mode 100644 Sources/TokamakCore/Modifiers/_HoverRegionModifier.swift create mode 100644 Sources/TokamakCore/ViewTree.swift create mode 100644 Sources/TokamakDOM/Modifiers/_HoverRegionModifier.swift create mode 100644 Sources/TokamakDemo/HoverDemo.swift diff --git a/Package.resolved b/Package.resolved index b16a3210..e4a46de2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -19,6 +19,15 @@ "version": null } }, + { + "package": "pure-swift-json", + "repositoryURL": "https://github.com/fabianfett/pure-swift-json.git", + "state": { + "branch": null, + "revision": "5dc8ec1d857f3b56e3a718a01165ae80c7677d48", + "version": "0.4.0" + } + }, { "package": "Runtime", "repositoryURL": "https://github.com/MaxDesiatov/Runtime.git", diff --git a/Package.swift b/Package.swift index a2e9ca67..5d152a2e 100644 --- a/Package.swift +++ b/Package.swift @@ -40,6 +40,7 @@ let package = Package( .package(url: "https://github.com/kateinoigakukun/JavaScriptKit.git", .revision("c90e82f")), .package(url: "https://github.com/MaxDesiatov/Runtime.git", .branch("wasi-build")), .package(url: "https://github.com/MaxDesiatov/OpenCombine.git", .branch("observable-object")), + .package(url: "https://github.com/fabianfett/pure-swift-json.git", from: "0.4.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define @@ -66,7 +67,13 @@ let package = Package( ), .target( name: "TokamakDOM", - dependencies: ["CombineShim", "JavaScriptKit", "TokamakCore", "TokamakStaticHTML"] + dependencies: [ + "CombineShim", + "JavaScriptKit", + "TokamakCore", + "TokamakStaticHTML", + .product(name: "PureSwiftJSON", package: "pure-swift-json"), + ] ), .target( name: "TokamakShim", diff --git a/Sources/TokamakCore/Modifiers/ViewModifier.swift b/Sources/TokamakCore/Modifiers/ViewModifier.swift index 422b0740..41604111 100644 --- a/Sources/TokamakCore/Modifiers/ViewModifier.swift +++ b/Sources/TokamakCore/Modifiers/ViewModifier.swift @@ -22,8 +22,8 @@ public struct _ViewModifier_Content: View where Modifier: ViewModifier public let modifier: Modifier public let view: AnyView - public var body: Never { - neverBody("_ViewModifier_Content") + public var body: AnyView { + view } } @@ -38,3 +38,17 @@ extension ViewModifier where Body == Never { fatalError("\(self) is a primitive `ViewModifier`, you're not supposed to run `body(content:)`") } } + +public protocol ViewModifierDeferredToRenderer { + func deferredBody(content: Content) -> AnyView where Content: View +} + +public struct _ViewModifierProxy { + let subject: Modifier + + public init(_ subject: Modifier) { self.subject = subject } + + public func body(content: Content) -> Modifier.Body { + subject.body(content: .init(modifier: subject, view: AnyView(content))) + } +} diff --git a/Sources/TokamakCore/Modifiers/_HoverRegionModifier.swift b/Sources/TokamakCore/Modifiers/_HoverRegionModifier.swift new file mode 100644 index 00000000..999c8b89 --- /dev/null +++ b/Sources/TokamakCore/Modifiers/_HoverRegionModifier.swift @@ -0,0 +1,33 @@ +// Copyright 2020 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Created by Carson Katri on 8/6/20. +// + +extension View { + public func onHover(perform action: @escaping (Bool) -> ()) -> some View { + modifier(_HoverRegionModifier(action)) + } +} + +public struct _HoverRegionModifier: ViewModifier { + public let callback: (Bool) -> () + public init(_ callback: @escaping (Bool) -> ()) { + self.callback = callback + } + + public func body(content: Content) -> some View { + content + } +} diff --git a/Sources/TokamakCore/MountedViews/MountedApp.swift b/Sources/TokamakCore/MountedViews/MountedApp.swift index 862220cf..1dc32a5f 100644 --- a/Sources/TokamakCore/MountedViews/MountedApp.swift +++ b/Sources/TokamakCore/MountedViews/MountedApp.swift @@ -28,10 +28,18 @@ final class MountedApp: MountedCompositeElement { let child: MountedElement = mountChild(childBody) mountedChildren = [child] child.mount(with: reconciler) + #if DEBUG + reconciler.debugTree[ObjectIdentifier(child)] = child.debugNode(parent: self) + #endif } override func unmount(with reconciler: StackReconciler) { - mountedChildren.forEach { $0.unmount(with: reconciler) } + mountedChildren.forEach { + $0.unmount(with: reconciler) + #if DEBUG + reconciler.debugTree[ObjectIdentifier($0)] = nil + #endif + } } private func mountChild(_ childBody: _AnyScene) -> MountedElement { @@ -56,4 +64,15 @@ final class MountedApp: MountedCompositeElement { mountChild: { mountChild($0) } ) } + + override func debugNode(parent: MountedElement? = nil) -> ViewTree.Node { + // swiftlint:disable:next force_try + let info = try! typeInfo(of: app.type) + return .init(type: app.type, + isPrimitive: false, + isHost: false, + dynamicProperties: info.properties.filter { $0.type is DynamicProperty.Type }.map(\.name), + object: self, + parent: parent) + } } diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift index e720a4eb..fc1dea05 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift @@ -29,6 +29,9 @@ final class MountedCompositeView: MountedCompositeElement { let child: MountedElement = childBody.makeMountedView(parentTarget, environmentValues) mountedChildren = [child] child.mount(with: reconciler) + #if DEBUG + reconciler.debugTree[ObjectIdentifier(child)] = child.debugNode(parent: self) + #endif // `_TargetRef` is a composite view, so it's enough to check for it only here if var targetRef = view.view as? TargetRefType { @@ -47,7 +50,12 @@ final class MountedCompositeView: MountedCompositeElement { } override func unmount(with reconciler: StackReconciler) { - mountedChildren.forEach { $0.unmount(with: reconciler) } + mountedChildren.forEach { + $0.unmount(with: reconciler) + #if DEBUG + reconciler.debugTree[ObjectIdentifier($0)] = nil + #endif + } if let appearanceAction = view.view as? AppearanceActionType { appearanceAction.disappear?() @@ -67,4 +75,15 @@ final class MountedCompositeView: MountedCompositeElement { mountChild: { $0.makeMountedView(parentTarget, environmentValues) } ) } + + override func debugNode(parent: MountedElement? = nil) -> ViewTree.Node { + // swiftlint:disable:next force_try + let info = try! typeInfo(of: view.type) + return .init(type: view.type, + isPrimitive: view.bodyType is Never.Type, + isHost: false, + dynamicProperties: info.properties.filter { $0.type is DynamicProperty.Type }.map(\.name), + object: self, + parent: parent) + } } diff --git a/Sources/TokamakCore/MountedViews/MountedElement.swift b/Sources/TokamakCore/MountedViews/MountedElement.swift index 774d5d6f..549ef279 100644 --- a/Sources/TokamakCore/MountedViews/MountedElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedElement.swift @@ -33,7 +33,7 @@ enum MountedElementKind { } public class MountedElement { - private var element: MountedElementKind + var element: MountedElementKind public internal(set) var app: _AnyApp { get { @@ -132,6 +132,10 @@ public class MountedElement { func update(with reconciler: StackReconciler) { fatalError("implement \(#function) in subclass") } + + func debugNode(parent: MountedElement? = nil) -> ViewTree.Node { + fatalError("implement \(#function) in subclass") + } } extension TypeInfo { diff --git a/Sources/TokamakCore/MountedViews/MountedEmptyView.swift b/Sources/TokamakCore/MountedViews/MountedEmptyView.swift index 8cf96ecb..497f4d1a 100644 --- a/Sources/TokamakCore/MountedViews/MountedEmptyView.swift +++ b/Sources/TokamakCore/MountedViews/MountedEmptyView.swift @@ -21,4 +21,12 @@ final class MountedEmptyView: MountedElement { override func unmount(with reconciler: StackReconciler) {} override func update(with reconciler: StackReconciler) {} + + override func debugNode(parent: MountedElement? = nil) -> ViewTree.Node { + .init(type: EmptyView.self, + isPrimitive: true, + isHost: false, + object: self, + parent: parent) + } } diff --git a/Sources/TokamakCore/MountedViews/MountedHostView.swift b/Sources/TokamakCore/MountedViews/MountedHostView.swift index fe082809..8c0e148f 100644 --- a/Sources/TokamakCore/MountedViews/MountedHostView.swift +++ b/Sources/TokamakCore/MountedViews/MountedHostView.swift @@ -48,7 +48,12 @@ public final class MountedHostView: MountedElement { mountedChildren = view.children.map { $0.makeMountedView(target, environmentValues) } - mountedChildren.forEach { $0.mount(with: reconciler) } + mountedChildren.forEach { + $0.mount(with: reconciler) + #if DEBUG + reconciler.debugTree[ObjectIdentifier($0)] = $0.debugNode(parent: self) + #endif + } } override func unmount(with reconciler: StackReconciler) { @@ -59,7 +64,12 @@ public final class MountedHostView: MountedElement { from: parentTarget, with: self ) { - self.mountedChildren.forEach { $0.unmount(with: reconciler) } + self.mountedChildren.forEach { + $0.unmount(with: reconciler) + #if DEBUG + reconciler.debugTree[ObjectIdentifier($0)] = nil + #endif + } } } @@ -132,4 +142,13 @@ public final class MountedHostView: MountedElement { case (true, true): () } } + + override func debugNode(parent: MountedElement? = nil) -> ViewTree.Node { + .init(type: view.type, + isPrimitive: view.bodyType is Never.Type, + isHost: true, + target: target, + object: self, + parent: parent) + } } diff --git a/Sources/TokamakCore/MountedViews/MountedScene.swift b/Sources/TokamakCore/MountedViews/MountedScene.swift index e2b75ab3..ae975630 100644 --- a/Sources/TokamakCore/MountedViews/MountedScene.swift +++ b/Sources/TokamakCore/MountedViews/MountedScene.swift @@ -35,10 +35,18 @@ final class MountedScene: MountedCompositeElement { let child: MountedElement = childBody.makeMountedElement(parentTarget, environmentValues) mountedChildren = [child] child.mount(with: reconciler) + #if DEBUG + reconciler.debugTree[ObjectIdentifier(child)] = child.debugNode(parent: self) + #endif } override func unmount(with reconciler: StackReconciler) { - mountedChildren.forEach { $0.unmount(with: reconciler) } + mountedChildren.forEach { + $0.unmount(with: reconciler) + #if DEBUG + reconciler.debugTree[ObjectIdentifier($0)] = nil + #endif + } } override func update(with reconciler: StackReconciler) { @@ -59,6 +67,17 @@ final class MountedScene: MountedCompositeElement { mountChild: { $0.makeMountedElement(parentTarget, environmentValues) } ) } + + override func debugNode(parent: MountedElement? = nil) -> ViewTree.Node { + // swiftlint:disable:next force_try + let info = try! typeInfo(of: scene.type) + return .init(type: scene.type, + isPrimitive: scene.bodyType is Never.Type, + isHost: false, + dynamicProperties: info.properties.filter { $0.type is DynamicProperty.Type }.map(\.name), + object: self, + parent: parent) + } } extension _AnyScene.BodyResult { diff --git a/Sources/TokamakCore/Renderer.swift b/Sources/TokamakCore/Renderer.swift index 2f49cbae..2e62e786 100644 --- a/Sources/TokamakCore/Renderer.swift +++ b/Sources/TokamakCore/Renderer.swift @@ -68,4 +68,14 @@ public protocol Renderer: AnyObject { with host: MountedHost, completion: @escaping () -> () ) + + func debugTree( + _ tree: Set.Node>, + context: ViewTree.Context + ) +} + +extension Renderer { + public func debugTree(_ tree: Set.Node>, + context: ViewTree.Context) {} } diff --git a/Sources/TokamakCore/StackReconciler.swift b/Sources/TokamakCore/StackReconciler.swift index 7b6b8820..2f4d17f6 100644 --- a/Sources/TokamakCore/StackReconciler.swift +++ b/Sources/TokamakCore/StackReconciler.swift @@ -63,6 +63,11 @@ public final class StackReconciler { */ private let scheduler: (@escaping () -> ()) -> () + #if DEBUG + typealias Tree = ViewTree + var debugTree: [ObjectIdentifier: Tree.Node] + #endif + public init( view: V, target: R.TargetType, @@ -76,7 +81,15 @@ public final class StackReconciler { rootElement = AnyView(view).makeMountedView(target, environment) + #if DEBUG + debugTree = [ObjectIdentifier(rootElement): rootElement.debugNode(parent: nil)] + #endif + rootElement.mount(with: self) + + #if DEBUG + renderer.debugTree(Set(debugTree.values), context: .firstRender) + #endif } public init( @@ -92,11 +105,19 @@ public final class StackReconciler { rootElement = MountedApp(app, target, environment) + #if DEBUG + debugTree = [ObjectIdentifier(rootElement): rootElement.debugNode(parent: nil)] + #endif + rootElement.mount(with: self) if let mountedApp = rootElement as? MountedApp { setupSubscription(for: app._phasePublisher, to: \.scenePhase, of: mountedApp) setupSubscription(for: app._colorSchemePublisher, to: \.colorScheme, of: mountedApp) } + + #if DEBUG + renderer.debugTree(Set(debugTree.values), context: .firstRender) + #endif } private func queueStateUpdate( @@ -118,10 +139,20 @@ public final class StackReconciler { } private func updateStateAndReconcile() { + #if DEBUG + let preValues = Set(debugTree.values) + #endif + for mountedView in queuedRerenders { mountedView.update(with: self) } + #if DEBUG + // Only send the difference for performance reasons + renderer?.debugTree(Set(debugTree.values).subtracting(preValues), + context: .update) + #endif + queuedRerenders.removeAll() } diff --git a/Sources/TokamakCore/Target.swift b/Sources/TokamakCore/Target.swift index fcd683a7..c1488cd7 100644 --- a/Sources/TokamakCore/Target.swift +++ b/Sources/TokamakCore/Target.swift @@ -17,4 +17,10 @@ public protocol Target: AnyObject { var view: AnyView { get set } + /// Provide a way to identify this Target from the debug tree. + var debugIdentifier: String? { get } +} + +extension Target { + public var debugIdentifier: String? { nil } } diff --git a/Sources/TokamakCore/ViewTree.swift b/Sources/TokamakCore/ViewTree.swift new file mode 100644 index 00000000..b51f33d9 --- /dev/null +++ b/Sources/TokamakCore/ViewTree.swift @@ -0,0 +1,66 @@ +// Copyright 2018-2020 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Created by Carson Katri on 8/5/20. +// + +import Runtime + +extension ObjectIdentifier: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(hashValue) + } +} + +public final class ViewTree { + public enum Context { + case firstRender + case update + } + + public struct Node: Encodable, Hashable { + let type: String + let isPrimitive: Bool + let isHost: Bool + let dynamicProperties: [String] + let target: String? + var id: ObjectIdentifier + var parent: ObjectIdentifier? + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id + } + + init(type: Any.Type, + isPrimitive: Bool, + isHost: Bool, + dynamicProperties: [String] = [], + target: R.TargetType? = nil, + object: MountedElement, + parent: MountedElement? = nil) { + self.type = "\(type)" + id = ObjectIdentifier(object) + self.isPrimitive = isPrimitive + self.isHost = isHost + self.dynamicProperties = dynamicProperties + self.target = target?.debugIdentifier + if let parent = parent { + self.parent = ObjectIdentifier(parent) + } else { + self.parent = nil + } + } + } +} diff --git a/Sources/TokamakDOM/DOMNode.swift b/Sources/TokamakDOM/DOMNode.swift index 344cfc99..20d40b05 100644 --- a/Sources/TokamakDOM/DOMNode.swift +++ b/Sources/TokamakDOM/DOMNode.swift @@ -42,10 +42,15 @@ final class DOMNode: Target { private var listeners: [String: JSClosure] var view: AnyView + let debugIdentifier: String? + init(_ view: V, _ ref: JSObjectRef, _ listeners: [String: Listener] = [:]) { self.ref = ref self.listeners = [:] self.view = AnyView(view) + + debugIdentifier = ref.id.string + reinstall(listeners) } @@ -53,6 +58,8 @@ final class DOMNode: Target { self.ref = ref view = AnyView(EmptyView()) listeners = [:] + + debugIdentifier = ref.id.string } /// Removes all existing event listeners on this DOM node and install new ones from diff --git a/Sources/TokamakDOM/DOMRenderer.swift b/Sources/TokamakDOM/DOMRenderer.swift index afedb913..f9cafe8a 100644 --- a/Sources/TokamakDOM/DOMRenderer.swift +++ b/Sources/TokamakDOM/DOMRenderer.swift @@ -16,6 +16,7 @@ // import JavaScriptKit +import PureSwiftJSON import TokamakCore import TokamakStaticHTML @@ -126,6 +127,8 @@ final class DOMRenderer: Renderer { lastChild.style.object!.height = "100%" } + lastChild.id = "\(ObjectIdentifier(host).hashValue)".jsValue() + if let dynamicHTML = anyHTML as? AnyDynamicHTML { return DOMNode(host.view, lastChild, dynamicHTML.listeners) } else { @@ -153,4 +156,23 @@ final class DOMRenderer: Renderer { _ = parent.ref.removeChild!(target.ref) } + + public func debugTree( + _ tree: Set.Node>, + context: ViewTree.Context + ) { + if tree.count > 0, + let json = try? PSJSONEncoder().encode(tree) { + switch context { + case .firstRender: + JSObjectRef.global.window.object!._tokamak_debug_tree = json.jsValue() + case .update: + let Object = JSObjectRef.global.Object.function! + let details = Object.new() + details.detail = json.jsValue() + let event = window.CustomEvent.function!.new("_tokamak_debug_tree_update", details) + _ = document.body.object!.dispatchEvent!(event) + } + } + } } diff --git a/Sources/TokamakDOM/Modifiers/_HoverRegionModifier.swift b/Sources/TokamakDOM/Modifiers/_HoverRegionModifier.swift new file mode 100644 index 00000000..5975c2d8 --- /dev/null +++ b/Sources/TokamakDOM/Modifiers/_HoverRegionModifier.swift @@ -0,0 +1,27 @@ +// Copyright 2020 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Created by Carson Katri on 8/6/20. +// + +import TokamakCore + +extension _HoverRegionModifier: ViewModifierDeferredToRenderer { + public func deferredBody(content: Content) -> AnyView where Content: View { + AnyView(DynamicHTML("div", listeners: [ + "mouseover": { _ in callback(true) }, + "mouseout": { _ in callback(false) }, + ]) { _ViewModifierProxy(self).body(content: content) }) + } +} diff --git a/Sources/TokamakDemo/HoverDemo.swift b/Sources/TokamakDemo/HoverDemo.swift new file mode 100644 index 00000000..b0817bad --- /dev/null +++ b/Sources/TokamakDemo/HoverDemo.swift @@ -0,0 +1,18 @@ +// +// File.swift +// +// +// Created by Carson Katri on 8/7/20. +// + +import TokamakShim + +struct HoverDemo: View { + @State private var isHovering = false + var body: some View { + Text(isHovering ? "Hovering" : "Not Hovering") + .onHover { + isHovering = $0 + } + } +} diff --git a/Sources/TokamakDemo/TokamakDemo.swift b/Sources/TokamakDemo/TokamakDemo.swift index 87823ab5..6d8adfe4 100644 --- a/Sources/TokamakDemo/TokamakDemo.swift +++ b/Sources/TokamakDemo/TokamakDemo.swift @@ -121,6 +121,9 @@ struct TokamakDemoView: View { NavItem("Text", destination: TextDemo()) NavItem("TextField", destination: TextFieldDemo()) } + Section(header: Text("Actions")) { + NavItem("onHover", destination: HoverDemo()) + } Section(header: Text("Misc")) { NavItem("Path", destination: PathDemo()) NavItem("Environment", destination: EnvironmentDemo().font(.system(size: 8))) diff --git a/Sources/TokamakStaticHTML/Modifiers/ModifiedContent.swift b/Sources/TokamakStaticHTML/Modifiers/ModifiedContent.swift index 6f30d65b..e53a4f42 100644 --- a/Sources/TokamakStaticHTML/Modifiers/ModifiedContent.swift +++ b/Sources/TokamakStaticHTML/Modifiers/ModifiedContent.swift @@ -50,6 +50,8 @@ extension ModifiedContent: ViewDeferredToRenderer where Content: View { content }) } + } else if let viewModifier = modifier as? ViewModifierDeferredToRenderer { + return viewModifier.deferredBody(content: content) } else { return AnyView(content) } diff --git a/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift b/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift index 8299aed8..26b678e6 100644 --- a/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift +++ b/Sources/TokamakStaticHTML/Resources/TokamakStyles.swift @@ -109,6 +109,22 @@ public let tokamakStyles = """ border-top-color: rgba(255, 255, 255, 0.25); } } + +._tokamak-debug-hover { + position: relative; +} +._tokamak-debug-hover::after { + background-color: rgb(100, 178, 200); + opacity: 0.6; + position: absolute; + top: 0; + left: 0; + display: block; + bottom: 0; + right: 0; + content: ''; + z-index: 999; +} """ public let rootNodeStyles = """