dsh-notch 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/BASELINE.md +13 -0
  2. package/DESIGN.md +34 -0
  3. package/LICENSE +21 -0
  4. package/README.en.md +136 -0
  5. package/README.md +145 -0
  6. package/cordis.yml +5 -0
  7. package/docs/motion-audit.md +65 -0
  8. package/docs/motion-continuity.md +77 -0
  9. package/docs/releases/v0.3.0.md +20 -0
  10. package/dshx.yml +14 -0
  11. package/macos/Package.swift +17 -0
  12. package/macos/STATUS-MOTION.md +49 -0
  13. package/macos/Sources/Client.swift +139 -0
  14. package/macos/Sources/IdleRobot.swift +423 -0
  15. package/macos/Sources/NotchMarkdown.swift +266 -0
  16. package/macos/Sources/Panel.swift +153 -0
  17. package/macos/Sources/Resources/Idle/balance.json +1 -0
  18. package/macos/Sources/Resources/Idle/blink.json +1 -0
  19. package/macos/Sources/Resources/Idle/cube-in.json +1 -0
  20. package/macos/Sources/Resources/Idle/dance.json +1 -0
  21. package/macos/Sources/Resources/Idle/hop.json +1 -0
  22. package/macos/Sources/Resources/Idle/nod.json +1 -0
  23. package/macos/Sources/Resources/Idle/satellite-in.json +1 -0
  24. package/macos/Sources/Resources/Idle/satellite-out.json +1 -0
  25. package/macos/Sources/Resources/Idle/scan.json +1 -0
  26. package/macos/Sources/Resources/Idle/sleep.json +1 -0
  27. package/macos/Sources/Resources/Idle/sneeze.json +1 -0
  28. package/macos/Sources/Resources/Idle/stretch.json +1 -0
  29. package/macos/Sources/Resources/Idle/tilt.json +1 -0
  30. package/macos/Sources/ReviewStudio.swift +337 -0
  31. package/macos/Sources/RootView.swift +1093 -0
  32. package/macos/Sources/StatusOrbit.swift +642 -0
  33. package/macos/Sources/main.swift +153 -0
  34. package/macos/Tests/ExpandedHeightProbe.swift +108 -0
  35. package/macos/Tests/GeometryProbe.swift +98 -0
  36. package/macos/Tests/IdleProbe.swift +294 -0
  37. package/macos/Tests/MotionProbe.swift +106 -0
  38. package/macos/Tests/OutcomeProbe.swift +133 -0
  39. package/macos/Tests/ScrollbarProbe.swift +91 -0
  40. package/macos/Tests/expanded-height.sh +13 -0
  41. package/macos/Tests/geometry.sh +14 -0
  42. package/macos/Tests/idle.sh +14 -0
  43. package/macos/Tests/motion.sh +14 -0
  44. package/macos/Tests/outcome.sh +14 -0
  45. package/macos/Tests/scrollbar.sh +13 -0
  46. package/package.json +46 -0
  47. package/src/board.ts +299 -0
  48. package/src/browse-sync.ts +59 -0
  49. package/src/dsh-notch.ts +60 -0
  50. package/src/http.ts +215 -0
  51. package/src/session-state.ts +31 -0
  52. package/src/store.ts +49 -0
  53. package/src/types.ts +65 -0
  54. package/tests/answer-lifecycle.test.mjs +23 -0
  55. package/tests/board.test.mjs +7 -0
  56. package/tests/browse-sync.test.mjs +20 -0
  57. package/tests/mirror.test.mjs +4 -0
  58. package/tests/peer-range.test.mjs +21 -0
  59. package/tests/session-state.test.mjs +54 -0
  60. package/tests/unread-hold.test.mjs +90 -0
  61. package/tools/desktop-shell/README.md +56 -0
  62. package/tools/desktop-shell/child-log.mjs +6 -0
  63. package/tools/desktop-shell/child-log.test.mjs +14 -0
  64. package/tools/desktop-shell/package-recovery.py +78 -0
  65. package/tools/desktop-shell/recovery-probe.cjs +50 -0
  66. package/tools/desktop-shell/renderer-recovery.mjs +95 -0
  67. package/tools/desktop-shell/renderer-recovery.test.mjs +24 -0
  68. package/tools/diagnostics/completion-watch.py +101 -0
  69. package/tools/diagnostics/completion-watch.test.py +17 -0
  70. package/tools/diagnostics/renderer-diagnostics.mjs +56 -0
  71. package/tools/diagnostics/renderer-diagnostics.test.mjs +32 -0
  72. package/tools/idle/LICENSE.open-bot-motion +21 -0
  73. package/tools/idle/export.mjs +40 -0
  74. package/tools/idle/index.html +4 -0
  75. package/tools/idle/motions.js +83 -0
  76. package/tools/idle/original.js +1673 -0
  77. package/tools/idle/renderer.js +1673 -0
  78. package/tools/recording/Client.swift +76 -0
  79. package/tools/recording/DemoCatalog.swift +174 -0
  80. package/tools/recording/README.md +23 -0
  81. package/tools/recording/build.sh +41 -0
  82. package/tools/recording/main.swift +100 -0
  83. package/tools/review/ReviewStudio.swift +236 -0
  84. package/tools/review/audit.sh +19 -0
  85. package/tools/review/build.sh +7 -0
  86. package/tools/review/main.swift +158 -0
  87. package/tools/review/render-audit.swift +32 -0
@@ -0,0 +1,266 @@
1
+ import AppKit
2
+ import SwiftUI
3
+
4
+ /// Block Markdown using DSH body type: 14pt system, 12pt SF Mono.
5
+ enum NotchMarkdown {
6
+ static let bodySize: CGFloat = 14
7
+ static let headingSize: CGFloat = 17
8
+ static let codeSize: CGFloat = 12
9
+ }
10
+
11
+ struct NotchMarkdownView: View {
12
+ let source: String
13
+
14
+ var body: some View {
15
+ VStack(alignment: .leading, spacing: 10) {
16
+ ForEach(Array(blocks.enumerated()), id: \.offset) { _, block in
17
+ blockView(block)
18
+ }
19
+ }
20
+ .frame(maxWidth: .infinity, alignment: .leading)
21
+ }
22
+
23
+ private var blocks: [Block] { Block.parse(source) }
24
+
25
+ @ViewBuilder
26
+ private func blockView(_ block: Block) -> some View {
27
+ switch block {
28
+ case .heading(let level, let text):
29
+ inline(text)
30
+ .font(.system(size: level <= 1 ? 18 : level == 2 ? 16 : 15, weight: .semibold))
31
+ .foregroundStyle(.white)
32
+ .padding(.top, level <= 2 ? 8 : 4)
33
+ case .paragraph(let text):
34
+ inline(text)
35
+ .font(.system(size: NotchMarkdown.bodySize))
36
+ .foregroundStyle(Color.white.opacity(0.9))
37
+ .lineSpacing(6)
38
+ case .list(let items, let ordered):
39
+ VStack(alignment: .leading, spacing: 4) {
40
+ ForEach(Array(items.enumerated()), id: \.offset) { i, item in
41
+ HStack(alignment: .top, spacing: 8) {
42
+ Text(ordered ? "\(i + 1)." : "•")
43
+ .font(.system(size: NotchMarkdown.bodySize, weight: .medium))
44
+ .foregroundStyle(Color.white.opacity(0.55))
45
+ .frame(width: 18, alignment: .trailing)
46
+ inline(item)
47
+ .font(.system(size: NotchMarkdown.bodySize))
48
+ .foregroundStyle(Color.white.opacity(0.9))
49
+ .lineSpacing(5)
50
+ }
51
+ }
52
+ }
53
+ case .table(let headers, let rows):
54
+ table(headers: headers, rows: rows)
55
+ case .code(let text):
56
+ Text(text)
57
+ .font(.system(size: NotchMarkdown.codeSize, design: .monospaced))
58
+ .foregroundStyle(Color.white.opacity(0.92))
59
+ .frame(maxWidth: .infinity, alignment: .leading)
60
+ .padding(10)
61
+ .background(
62
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
63
+ .fill(Color.white.opacity(0.07))
64
+ )
65
+ case .quote(let text):
66
+ inline(text)
67
+ .font(.system(size: NotchMarkdown.bodySize))
68
+ .foregroundStyle(Color.white.opacity(0.7))
69
+ .padding(.leading, 10)
70
+ .overlay(alignment: .leading) {
71
+ Rectangle().fill(Color.white.opacity(0.28)).frame(width: 2)
72
+ }
73
+ case .rule:
74
+ Divider().overlay(Color.white.opacity(0.18))
75
+ }
76
+ }
77
+
78
+ private func inline(_ markdown: String) -> Text {
79
+ var options = AttributedString.MarkdownParsingOptions()
80
+ options.interpretedSyntax = .inlineOnlyPreservingWhitespace
81
+ options.failurePolicy = .returnPartiallyParsedIfPossible
82
+ if let parsed = try? AttributedString(markdown: markdown, options: options) {
83
+ var styled = parsed
84
+ for run in styled.runs {
85
+ if run.inlinePresentationIntent?.contains(.code) == true {
86
+ styled[run.range].font = .system(size: NotchMarkdown.codeSize, design: .monospaced)
87
+ styled[run.range].backgroundColor = NSColor.white.withAlphaComponent(0.1)
88
+ }
89
+ }
90
+ return Text(styled)
91
+ }
92
+ return Text(markdown)
93
+ }
94
+
95
+ private func table(headers: [String], rows: [[String]]) -> some View {
96
+ let columns = max(headers.count, rows.map(\.count).max() ?? 0)
97
+ return VStack(alignment: .leading, spacing: 0) {
98
+ if !headers.isEmpty {
99
+ tableRow(cells: headers, header: true, columns: columns)
100
+ Rectangle().fill(Color.white.opacity(0.2)).frame(height: 1)
101
+ }
102
+ ForEach(Array(rows.enumerated()), id: \.offset) { i, row in
103
+ tableRow(cells: row, header: false, columns: columns)
104
+ if i < rows.count - 1 {
105
+ Rectangle().fill(Color.white.opacity(0.08)).frame(height: 1)
106
+ }
107
+ }
108
+ }
109
+ .padding(8)
110
+ .background(
111
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
112
+ .stroke(Color.white.opacity(0.16), lineWidth: 1)
113
+ )
114
+ }
115
+
116
+ private func tableRow(cells: [String], header: Bool, columns: Int) -> some View {
117
+ HStack(alignment: .top, spacing: 0) {
118
+ ForEach(0..<columns, id: \.self) { i in
119
+ let cell = i < cells.count ? cells[i] : ""
120
+ Text(cell)
121
+ .font(.system(size: 12, weight: header ? .semibold : .regular))
122
+ .foregroundStyle(Color.white.opacity(header ? 0.95 : 0.82))
123
+ .frame(maxWidth: .infinity, alignment: .leading)
124
+ .padding(.vertical, 5)
125
+ .padding(.horizontal, 6)
126
+ if i < columns - 1 {
127
+ Rectangle().fill(Color.white.opacity(0.1)).frame(width: 1)
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ private enum Block {
135
+ case heading(Int, String)
136
+ case paragraph(String)
137
+ case list([String], Bool)
138
+ case table([String], [[String]])
139
+ case code(String)
140
+ case quote(String)
141
+ case rule
142
+
143
+ static func parse(_ source: String) -> [Block] {
144
+ let lines = source.replacingOccurrences(of: "\r\n", with: "\n").split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
145
+ var blocks: [Block] = []
146
+ var i = 0
147
+ while i < lines.count {
148
+ let raw = lines[i]
149
+ let line = raw.trimmingCharacters(in: .whitespaces)
150
+ if line.isEmpty { i += 1; continue }
151
+ if line.hasPrefix("```") {
152
+ i += 1
153
+ var body: [String] = []
154
+ while i < lines.count, !lines[i].trimmingCharacters(in: .whitespaces).hasPrefix("```") {
155
+ body.append(lines[i])
156
+ i += 1
157
+ }
158
+ if i < lines.count { i += 1 }
159
+ blocks.append(.code(body.joined(separator: "\n")))
160
+ continue
161
+ }
162
+ if line == "---" || line == "***" || line == "___" {
163
+ blocks.append(.rule)
164
+ i += 1
165
+ continue
166
+ }
167
+ if let heading = heading(line) {
168
+ blocks.append(heading)
169
+ i += 1
170
+ continue
171
+ }
172
+ if line.hasPrefix("|") && i + 1 < lines.count, isTableDivider(lines[i + 1]) {
173
+ let headers = cells(line)
174
+ i += 2
175
+ var rows: [[String]] = []
176
+ while i < lines.count, lines[i].trimmingCharacters(in: .whitespaces).hasPrefix("|") {
177
+ rows.append(cells(lines[i]))
178
+ i += 1
179
+ }
180
+ blocks.append(.table(headers, rows))
181
+ continue
182
+ }
183
+ if line.hasPrefix("> ") || line == ">" {
184
+ var quoted: [String] = []
185
+ while i < lines.count {
186
+ let t = lines[i].trimmingCharacters(in: .whitespaces)
187
+ if t.hasPrefix("> ") { quoted.append(String(t.dropFirst(2))) }
188
+ else if t == ">" { quoted.append("") }
189
+ else { break }
190
+ i += 1
191
+ }
192
+ blocks.append(.quote(quoted.joined(separator: "\n")))
193
+ continue
194
+ }
195
+ if isBullet(line) || isOrdered(line) {
196
+ let ordered = isOrdered(line)
197
+ var items: [String] = []
198
+ while i < lines.count {
199
+ let t = lines[i].trimmingCharacters(in: .whitespaces)
200
+ if ordered, let item = orderedItem(t) { items.append(item) }
201
+ else if !ordered, let item = bulletItem(t) { items.append(item) }
202
+ else { break }
203
+ i += 1
204
+ }
205
+ blocks.append(.list(items, ordered))
206
+ continue
207
+ }
208
+ var para: [String] = [line]
209
+ i += 1
210
+ while i < lines.count {
211
+ let t = lines[i].trimmingCharacters(in: .whitespaces)
212
+ if t.isEmpty || t.hasPrefix("#") || t.hasPrefix("|") || t.hasPrefix("```") || t.hasPrefix("> ") || isBullet(t) || isOrdered(t) || t == "---" { break }
213
+ para.append(t)
214
+ i += 1
215
+ }
216
+ blocks.append(.paragraph(para.joined(separator: " ")))
217
+ }
218
+ return blocks
219
+ }
220
+
221
+ private static func heading(_ line: String) -> Block? {
222
+ guard line.hasPrefix("#") else { return nil }
223
+ var level = 0
224
+ for ch in line {
225
+ if ch == "#" { level += 1 } else { break }
226
+ }
227
+ guard level > 0, level <= 6, line.count > level, line[line.index(line.startIndex, offsetBy: level)] == " " else { return nil }
228
+ return .heading(level, String(line.dropFirst(level + 1)))
229
+ }
230
+
231
+ private static func isTableDivider(_ line: String) -> Bool {
232
+ let t = line.trimmingCharacters(in: .whitespaces)
233
+ guard t.hasPrefix("|") else { return false }
234
+ return t.replacingOccurrences(of: "|", with: "").replacingOccurrences(of: ":", with: "").replacingOccurrences(of: "-", with: "").replacingOccurrences(of: " ", with: "").isEmpty
235
+ }
236
+
237
+ private static func cells(_ line: String) -> [String] {
238
+ var parts = line.split(separator: "|", omittingEmptySubsequences: false).map { $0.trimmingCharacters(in: .whitespaces) }
239
+ if parts.first == "" { parts.removeFirst() }
240
+ if parts.last == "" { parts.removeLast() }
241
+ return parts
242
+ }
243
+
244
+ private static func isBullet(_ line: String) -> Bool {
245
+ line.hasPrefix("- ") || line.hasPrefix("* ")
246
+ }
247
+
248
+ private static func bulletItem(_ line: String) -> String? {
249
+ if line.hasPrefix("- ") { return String(line.dropFirst(2)) }
250
+ if line.hasPrefix("* ") { return String(line.dropFirst(2)) }
251
+ return nil
252
+ }
253
+
254
+ private static func isOrdered(_ line: String) -> Bool {
255
+ orderedItem(line) != nil
256
+ }
257
+
258
+ private static func orderedItem(_ line: String) -> String? {
259
+ guard let dot = line.firstIndex(of: ".") else { return nil }
260
+ let n = line[..<dot]
261
+ guard !n.isEmpty, n.allSatisfy(\.isNumber) else { return nil }
262
+ let rest = line[line.index(after: dot)...]
263
+ guard rest.first == " " else { return nil }
264
+ return String(rest.dropFirst())
265
+ }
266
+ }
@@ -0,0 +1,153 @@
1
+ import AppKit
2
+ import SwiftUI
3
+
4
+ enum NotchGeometryAnimation {
5
+ static let animation = Animation.spring(duration: 0.4, bounce: 0.08)
6
+ static let duration: TimeInterval = 0.4
7
+ static func progress(_ t: Double) -> Double {
8
+ let x = min(1, max(0, t))
9
+ return x*x*x*(x*(6*x-15)+10)
10
+ }
11
+ }
12
+
13
+ final class NotchPanel: NSPanel {
14
+ private var resizeTimer: Timer?
15
+ private var resizeTarget: NSSize?
16
+ private var resizeGeneration = 0
17
+
18
+ func cancelResize() {
19
+ resizeTimer?.invalidate()
20
+ resizeTimer = nil
21
+ resizeTarget = nil
22
+ resizeGeneration += 1
23
+ }
24
+
25
+ /// Animate one native rectangle, preserving its top-right edge at every frame.
26
+ func resizeAnchored(to size: NSSize, animated: Bool = true) {
27
+ guard resizeTarget != size else { return }
28
+ cancelResize()
29
+ let generation = resizeGeneration
30
+ resizeTarget = size
31
+ let start = frame
32
+ let end = NSRect(x: start.maxX - size.width, y: start.maxY - size.height, width: size.width, height: size.height)
33
+ guard animated, !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion else {
34
+ setFrame(end, display: true)
35
+ return
36
+ }
37
+ if #available(macOS 15.0, *) {
38
+ NSAnimationContext.animate(NotchGeometryAnimation.animation) {
39
+ self.animator().setFrame(end, display: true)
40
+ }
41
+ return
42
+ }
43
+ let began = ProcessInfo.processInfo.systemUptime
44
+ resizeTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 60.0, repeats: true) { [weak self] _ in
45
+ Task { @MainActor in
46
+ guard let self, self.resizeGeneration == generation else { return }
47
+ let t = min(1, (ProcessInfo.processInfo.systemUptime - began) / NotchGeometryAnimation.duration)
48
+ let progress = NotchGeometryAnimation.progress(t)
49
+ let width = start.width + (size.width - start.width) * progress
50
+ let height = start.height + (size.height - start.height) * progress
51
+ self.setFrame(NSRect(x: start.maxX - width, y: start.maxY - height, width: width, height: height), display: true)
52
+ if t >= 1 { self.resizeTimer?.invalidate(); self.resizeTimer = nil }
53
+ }
54
+ }
55
+ }
56
+
57
+ override var canBecomeKey: Bool { true }
58
+ override var canBecomeMain: Bool { false }
59
+
60
+ // An accessory panel has no active app Edit menu to route Command keys.
61
+ // Handle only standard editing commands, and only for our focused editor.
62
+ override func performKeyEquivalent(with event: NSEvent) -> Bool {
63
+ guard isKeyWindow, let editor = firstResponder as? NSTextView else {
64
+ return super.performKeyEquivalent(with: event)
65
+ }
66
+ let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
67
+ guard flags.contains(.command), !flags.contains(.control), !flags.contains(.option) else {
68
+ return super.performKeyEquivalent(with: event)
69
+ }
70
+ let key = event.charactersIgnoringModifiers?.lowercased()
71
+ if flags.contains(.shift) {
72
+ if key == "z", editor.isEditable, let undo = editor.undoManager, undo.canRedo {
73
+ undo.redo()
74
+ return true
75
+ }
76
+ return super.performKeyEquivalent(with: event)
77
+ }
78
+ switch key {
79
+ case "a": editor.selectAll(nil)
80
+ case "c": editor.copy(nil)
81
+ case "x" where editor.isEditable: editor.cut(nil)
82
+ case "v" where editor.isEditable: editor.paste(nil)
83
+ case "z" where editor.isEditable:
84
+ guard let undo = editor.undoManager, undo.canUndo else {
85
+ return super.performKeyEquivalent(with: event)
86
+ }
87
+ undo.undo()
88
+ default: return super.performKeyEquivalent(with: event)
89
+ }
90
+ return true
91
+ }
92
+
93
+ convenience init(size: NSSize) {
94
+ self.init(
95
+ contentRect: NSRect(origin: .zero, size: size),
96
+ styleMask: [.borderless, .nonactivatingPanel],
97
+ backing: .buffered,
98
+ defer: false
99
+ )
100
+ level = .statusBar
101
+ collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
102
+ isOpaque = false
103
+ backgroundColor = .clear
104
+ appearance = NSAppearance(named: .darkAqua)
105
+ hasShadow = false
106
+ isMovable = false
107
+ hidesOnDeactivate = false
108
+ becomesKeyOnlyIfNeeded = true
109
+ isReleasedWhenClosed = false
110
+ }
111
+
112
+ /// Put the SwiftUI host on top of a real window-backed HUD blur.
113
+ /// A VisualEffect inside NSHostingView is covered by the hosting view's opaque fill.
114
+ func embedHost(_ hosting: NSView) {
115
+ let effect = NSVisualEffectView(frame: contentView?.bounds ?? NSRect(origin: .zero, size: frame.size))
116
+ effect.material = .hudWindow
117
+ effect.blendingMode = .behindWindow
118
+ effect.state = .active
119
+ effect.isEmphasized = true
120
+ effect.autoresizingMask = [.width, .height]
121
+ effect.wantsLayer = true
122
+ effect.layer?.masksToBounds = true
123
+ effect.layer?.cornerRadius = 16
124
+ effect.layer?.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
125
+ effect.appearance = NSAppearance(named: .darkAqua)
126
+ contentView = effect
127
+ hosting.autoresizingMask = [.width, .height]
128
+ hosting.frame = effect.bounds
129
+ hosting.wantsLayer = true
130
+ hosting.layer?.isOpaque = false
131
+ hosting.layer?.backgroundColor = NSColor.clear.cgColor
132
+ effect.addSubview(hosting)
133
+ }
134
+ }
135
+
136
+ final class NotchHostingView<Content: View>: NSHostingView<Content> {
137
+ override var isOpaque: Bool { false }
138
+
139
+ // The panel is intentionally nonactivating. A click must reach SwiftUI
140
+ // controls immediately, even while another app is the active application.
141
+ override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
142
+ }
143
+
144
+ struct NotchScreenLayout {
145
+ let edgeInset: CGFloat
146
+ let maximumHeight: CGFloat
147
+
148
+ init(availableHeight: CGFloat, preferredInset: CGFloat = 100) {
149
+ let height = max(1, availableHeight)
150
+ edgeInset = min(preferredInset, max(0, (height - 1) / 2))
151
+ maximumHeight = height - 2 * edgeInset
152
+ }
153
+ }