|
|
@@ -22,6 +22,14 @@ private enum ProfilePagePalette {
|
|
22
|
22
|
|
|
23
|
23
|
/// Keeps profile text left-aligned and LTR so fields do not collapse to a narrow trailing strip under RTL / natural alignment.
|
|
24
|
24
|
private enum ProfileLayoutEnforcement {
|
|
|
25
|
+ static func applyForcedLTRSubtree(from root: NSView) {
|
|
|
26
|
+ var stack: [NSView] = [root]
|
|
|
27
|
+ while let view = stack.popLast() {
|
|
|
28
|
+ applyForcedLTR(to: view)
|
|
|
29
|
+ stack.append(contentsOf: view.subviews)
|
|
|
30
|
+ }
|
|
|
31
|
+ }
|
|
|
32
|
+
|
|
25
|
33
|
static func applyForcedLTR(to view: NSView) {
|
|
26
|
34
|
view.userInterfaceLayoutDirection = .leftToRight
|
|
27
|
35
|
}
|
|
|
@@ -44,6 +52,15 @@ private enum ProfileLayoutEnforcement {
|
|
44
|
52
|
}
|
|
45
|
53
|
}
|
|
46
|
54
|
|
|
|
55
|
+private extension NSStackView {
|
|
|
56
|
+ /// For vertical stacks using `.leading` alignment (geometric left under mixed RTL), pin each arranged subview’s width to the stack so labels/fields stay full-width.
|
|
|
57
|
+ func pinAllArrangedSubviewWidthsEqualToStackWidth() {
|
|
|
58
|
+ for subview in arrangedSubviews {
|
|
|
59
|
+ subview.widthAnchor.constraint(equalTo: widthAnchor).isActive = true
|
|
|
60
|
+ }
|
|
|
61
|
+ }
|
|
|
62
|
+}
|
|
|
63
|
+
|
|
47
|
64
|
/// Two fields side‑by‑side with a true 50/50 split, or stacked full‑width when compact. Avoids `NSStackView` collapsing paired columns to a narrow strip on the trailing edge.
|
|
48
|
65
|
private final class ProfileDualFieldRow: NSView {
|
|
49
|
66
|
private let leftView: NSView
|
|
|
@@ -158,6 +175,13 @@ final class MyProfilePageView: NSView {
|
|
158
|
175
|
setup()
|
|
159
|
176
|
}
|
|
160
|
177
|
|
|
|
178
|
+ override func viewDidMoveToWindow() {
|
|
|
179
|
+ super.viewDidMoveToWindow()
|
|
|
180
|
+ guard window != nil else { return }
|
|
|
181
|
+ ProfileLayoutEnforcement.applyForcedLTRSubtree(from: self)
|
|
|
182
|
+ needsLayout = true
|
|
|
183
|
+ }
|
|
|
184
|
+
|
|
161
|
185
|
override func layout() {
|
|
162
|
186
|
super.layout()
|
|
163
|
187
|
if let layer = cardView.layer, layer.shadowOpacity > 0 {
|
|
|
@@ -243,7 +267,7 @@ final class MyProfilePageView: NSView {
|
|
243
|
267
|
|
|
244
|
268
|
formStack.translatesAutoresizingMaskIntoConstraints = false
|
|
245
|
269
|
formStack.orientation = .vertical
|
|
246
|
|
- formStack.alignment = .width
|
|
|
270
|
+ formStack.alignment = .leading
|
|
247
|
271
|
formStack.distribution = .fill
|
|
248
|
272
|
formStack.spacing = 24
|
|
249
|
273
|
formStack.edgeInsets = NSEdgeInsets(top: 32, left: 28, bottom: 32, right: 28)
|
|
|
@@ -266,7 +290,6 @@ final class MyProfilePageView: NSView {
|
|
266
|
290
|
// Pin the document to the clip view’s geometric width so LTR/RTL semantics cannot slide the form.
|
|
267
|
291
|
documentView.leftAnchor.constraint(equalTo: scrollView.contentView.leftAnchor),
|
|
268
|
292
|
documentView.rightAnchor.constraint(equalTo: scrollView.contentView.rightAnchor),
|
|
269
|
|
- documentView.widthAnchor.constraint(equalTo: scrollView.contentView.widthAnchor),
|
|
270
|
293
|
documentView.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor),
|
|
271
|
294
|
documentView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: Self.horizontalPageInset),
|
|
272
|
295
|
|
|
|
@@ -341,6 +364,8 @@ final class MyProfilePageView: NSView {
|
|
341
|
364
|
|
|
342
|
365
|
appendWorkExperienceEntry()
|
|
343
|
366
|
appendEducationEntry()
|
|
|
367
|
+
|
|
|
368
|
+ ProfileLayoutEnforcement.applyForcedLTRSubtree(from: self)
|
|
344
|
369
|
}
|
|
345
|
370
|
|
|
346
|
371
|
private func applyResponsiveRowsIfNeeded() {
|
|
|
@@ -364,6 +389,7 @@ final class MyProfilePageView: NSView {
|
|
364
|
389
|
formStack.addArrangedSubview(view)
|
|
365
|
390
|
view.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
366
|
391
|
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
392
|
+ view.widthAnchor.constraint(equalTo: formStack.widthAnchor).isActive = true
|
|
367
|
393
|
}
|
|
368
|
394
|
|
|
369
|
395
|
private func sectionHeading(_ text: String) -> NSView {
|
|
|
@@ -398,7 +424,7 @@ final class MyProfilePageView: NSView {
|
|
398
|
424
|
label.font = .systemFont(ofSize: 12, weight: .medium)
|
|
399
|
425
|
label.textColor = ProfilePagePalette.secondaryText
|
|
400
|
426
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
401
|
|
- label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
|
427
|
+ label.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
402
|
428
|
ProfileLayoutEnforcement.applyLeftAlignedTextField(label)
|
|
403
|
429
|
|
|
404
|
430
|
styleSingleLineField(field, placeholder: placeholder)
|
|
|
@@ -407,8 +433,8 @@ final class MyProfilePageView: NSView {
|
|
407
|
433
|
let stack = NSStackView(views: [label, wrap])
|
|
408
|
434
|
stack.orientation = .vertical
|
|
409
|
435
|
stack.spacing = 8
|
|
410
|
|
- // `.width` stretches label + field chrome to the row width; `.leading` collapses to intrinsic width and caused right-edge compression.
|
|
411
|
|
- stack.alignment = .width
|
|
|
436
|
+ // `.leading` keeps rows on the geometric left; explicit widths keep labels/fields full-width (`.width` alone can still hug the trailing edge under RTL-style layout).
|
|
|
437
|
+ stack.alignment = .leading
|
|
412
|
438
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
413
|
439
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
414
|
440
|
ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
@@ -416,8 +442,9 @@ final class MyProfilePageView: NSView {
|
|
416
|
442
|
wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
417
|
443
|
wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
418
|
444
|
NSLayoutConstraint.activate([
|
|
419
|
|
- wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
420
|
|
- label.leftAnchor.constraint(equalTo: stack.leftAnchor)
|
|
|
445
|
+ label.leftAnchor.constraint(equalTo: stack.leftAnchor),
|
|
|
446
|
+ label.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
447
|
+ wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
|
|
421
|
448
|
])
|
|
422
|
449
|
return stack
|
|
423
|
450
|
}
|
|
|
@@ -523,14 +550,16 @@ final class MyProfilePageView: NSView {
|
|
523
|
550
|
let stack = NSStackView(views: [label, wrap])
|
|
524
|
551
|
stack.orientation = .vertical
|
|
525
|
552
|
stack.spacing = 8
|
|
526
|
|
- stack.alignment = .width
|
|
|
553
|
+ stack.alignment = .leading
|
|
527
|
554
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
528
|
555
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
529
|
556
|
ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
530
|
557
|
stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
531
|
558
|
wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
532
|
559
|
NSLayoutConstraint.activate([
|
|
533
|
|
- label.leftAnchor.constraint(equalTo: stack.leftAnchor)
|
|
|
560
|
+ label.leftAnchor.constraint(equalTo: stack.leftAnchor),
|
|
|
561
|
+ label.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
562
|
+ wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
|
|
534
|
563
|
])
|
|
535
|
564
|
return stack
|
|
536
|
565
|
}
|
|
|
@@ -590,14 +619,16 @@ final class MyProfilePageView: NSView {
|
|
590
|
619
|
let stack = NSStackView(views: [label, wrap])
|
|
591
|
620
|
stack.orientation = .vertical
|
|
592
|
621
|
stack.spacing = 8
|
|
593
|
|
- stack.alignment = .width
|
|
|
622
|
+ stack.alignment = .leading
|
|
594
|
623
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
595
|
624
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
596
|
625
|
ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
597
|
626
|
stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
598
|
627
|
wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
599
|
628
|
NSLayoutConstraint.activate([
|
|
600
|
|
- label.leftAnchor.constraint(equalTo: stack.leftAnchor)
|
|
|
629
|
+ label.leftAnchor.constraint(equalTo: stack.leftAnchor),
|
|
|
630
|
+ label.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
631
|
+ wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
|
|
601
|
632
|
])
|
|
602
|
633
|
return stack
|
|
603
|
634
|
}
|
|
|
@@ -622,7 +653,7 @@ final class MyProfilePageView: NSView {
|
|
622
|
653
|
let stack = NSStackView(views: [label, wrap, helper])
|
|
623
|
654
|
stack.orientation = .vertical
|
|
624
|
655
|
stack.spacing = 8
|
|
625
|
|
- stack.alignment = .width
|
|
|
656
|
+ stack.alignment = .leading
|
|
626
|
657
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
627
|
658
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
628
|
659
|
ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
@@ -633,7 +664,10 @@ final class MyProfilePageView: NSView {
|
|
633
|
664
|
}
|
|
634
|
665
|
NSLayoutConstraint.activate([
|
|
635
|
666
|
label.leftAnchor.constraint(equalTo: stack.leftAnchor),
|
|
636
|
|
- helper.leftAnchor.constraint(equalTo: stack.leftAnchor)
|
|
|
667
|
+ label.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
668
|
+ wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
669
|
+ helper.leftAnchor.constraint(equalTo: stack.leftAnchor),
|
|
|
670
|
+ helper.widthAnchor.constraint(equalTo: stack.widthAnchor)
|
|
637
|
671
|
])
|
|
638
|
672
|
return stack
|
|
639
|
673
|
}
|
|
|
@@ -677,17 +711,18 @@ final class MyProfilePageView: NSView {
|
|
677
|
711
|
workExperienceRowsStack.translatesAutoresizingMaskIntoConstraints = false
|
|
678
|
712
|
workExperienceRowsStack.orientation = .vertical
|
|
679
|
713
|
workExperienceRowsStack.spacing = 20
|
|
680
|
|
- workExperienceRowsStack.alignment = .width
|
|
|
714
|
+ workExperienceRowsStack.alignment = .leading
|
|
681
|
715
|
workExperienceRowsStack.userInterfaceLayoutDirection = .leftToRight
|
|
682
|
716
|
ProfileLayoutEnforcement.applyForcedLTR(to: workExperienceRowsStack)
|
|
683
|
717
|
|
|
684
|
718
|
let outer = NSStackView(views: [headerRow, workExperienceRowsStack])
|
|
685
|
719
|
outer.orientation = .vertical
|
|
686
|
720
|
outer.spacing = 16
|
|
687
|
|
- outer.alignment = .width
|
|
|
721
|
+ outer.alignment = .leading
|
|
688
|
722
|
outer.translatesAutoresizingMaskIntoConstraints = false
|
|
689
|
723
|
outer.userInterfaceLayoutDirection = .leftToRight
|
|
690
|
724
|
ProfileLayoutEnforcement.applyForcedLTR(to: outer)
|
|
|
725
|
+ outer.pinAllArrangedSubviewWidthsEqualToStackWidth()
|
|
691
|
726
|
return outer
|
|
692
|
727
|
}
|
|
693
|
728
|
|
|
|
@@ -722,17 +757,18 @@ final class MyProfilePageView: NSView {
|
|
722
|
757
|
educationRowsStack.translatesAutoresizingMaskIntoConstraints = false
|
|
723
|
758
|
educationRowsStack.orientation = .vertical
|
|
724
|
759
|
educationRowsStack.spacing = 16
|
|
725
|
|
- educationRowsStack.alignment = .width
|
|
|
760
|
+ educationRowsStack.alignment = .leading
|
|
726
|
761
|
educationRowsStack.userInterfaceLayoutDirection = .leftToRight
|
|
727
|
762
|
ProfileLayoutEnforcement.applyForcedLTR(to: educationRowsStack)
|
|
728
|
763
|
|
|
729
|
764
|
let outer = NSStackView(views: [headerRow, educationRowsStack])
|
|
730
|
765
|
outer.orientation = .vertical
|
|
731
|
766
|
outer.spacing = 16
|
|
732
|
|
- outer.alignment = .width
|
|
|
767
|
+ outer.alignment = .leading
|
|
733
|
768
|
outer.translatesAutoresizingMaskIntoConstraints = false
|
|
734
|
769
|
outer.userInterfaceLayoutDirection = .leftToRight
|
|
735
|
770
|
ProfileLayoutEnforcement.applyForcedLTR(to: outer)
|
|
|
771
|
+ outer.pinAllArrangedSubviewWidthsEqualToStackWidth()
|
|
736
|
772
|
return outer
|
|
737
|
773
|
}
|
|
738
|
774
|
|
|
|
@@ -748,6 +784,7 @@ final class MyProfilePageView: NSView {
|
|
748
|
784
|
}
|
|
749
|
785
|
workExperienceEntries.append(entry)
|
|
750
|
786
|
workExperienceRowsStack.addArrangedSubview(entry)
|
|
|
787
|
+ entry.widthAnchor.constraint(equalTo: workExperienceRowsStack.widthAnchor).isActive = true
|
|
751
|
788
|
renumberWorkExperienceEntries()
|
|
752
|
789
|
refreshWorkExperienceDeleteButtons()
|
|
753
|
790
|
}
|
|
|
@@ -786,6 +823,7 @@ final class MyProfilePageView: NSView {
|
|
786
|
823
|
}
|
|
787
|
824
|
educationEntries.append(entry)
|
|
788
|
825
|
educationRowsStack.addArrangedSubview(entry)
|
|
|
826
|
+ entry.widthAnchor.constraint(equalTo: educationRowsStack.widthAnchor).isActive = true
|
|
789
|
827
|
renumberEducationEntries()
|
|
790
|
828
|
refreshEducationDeleteButtons()
|
|
791
|
829
|
}
|
|
|
@@ -940,11 +978,12 @@ private final class WorkExperienceEntryView: NSView {
|
|
940
|
978
|
let inner = NSStackView(views: [headerRow, jobCompanyRow, durationGroup, descriptionGroup])
|
|
941
|
979
|
inner.orientation = .vertical
|
|
942
|
980
|
inner.spacing = 16
|
|
943
|
|
- inner.alignment = .width
|
|
|
981
|
+ inner.alignment = .leading
|
|
944
|
982
|
inner.translatesAutoresizingMaskIntoConstraints = false
|
|
945
|
983
|
inner.edgeInsets = NSEdgeInsets(top: 16, left: 18, bottom: 16, right: 18)
|
|
946
|
984
|
inner.userInterfaceLayoutDirection = .leftToRight
|
|
947
|
985
|
ProfileLayoutEnforcement.applyForcedLTR(to: inner)
|
|
|
986
|
+ inner.pinAllArrangedSubviewWidthsEqualToStackWidth()
|
|
948
|
987
|
|
|
949
|
988
|
addSubview(inner)
|
|
950
|
989
|
NSLayoutConstraint.activate([
|
|
|
@@ -987,7 +1026,7 @@ private final class WorkExperienceEntryView: NSView {
|
|
987
|
1026
|
let stack = NSStackView(views: [label, wrap])
|
|
988
|
1027
|
stack.orientation = .vertical
|
|
989
|
1028
|
stack.spacing = 8
|
|
990
|
|
- stack.alignment = .width
|
|
|
1029
|
+ stack.alignment = .leading
|
|
991
|
1030
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
992
|
1031
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
993
|
1032
|
ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
|
@@ -995,8 +1034,9 @@ private final class WorkExperienceEntryView: NSView {
|
|
995
|
1034
|
wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
996
|
1035
|
wrap.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
997
|
1036
|
NSLayoutConstraint.activate([
|
|
998
|
|
- wrap.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
999
|
|
- label.leftAnchor.constraint(equalTo: stack.leftAnchor)
|
|
|
1037
|
+ label.leftAnchor.constraint(equalTo: stack.leftAnchor),
|
|
|
1038
|
+ label.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
1039
|
+ wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
|
|
1000
|
1040
|
])
|
|
1001
|
1041
|
return stack
|
|
1002
|
1042
|
}
|
|
|
@@ -1053,14 +1093,16 @@ private final class WorkExperienceEntryView: NSView {
|
|
1053
|
1093
|
let stack = NSStackView(views: [label, wrap])
|
|
1054
|
1094
|
stack.orientation = .vertical
|
|
1055
|
1095
|
stack.spacing = 8
|
|
1056
|
|
- stack.alignment = .width
|
|
|
1096
|
+ stack.alignment = .leading
|
|
1057
|
1097
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
1058
|
1098
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
1059
|
1099
|
ProfileLayoutEnforcement.applyForcedLTR(to: stack)
|
|
1060
|
1100
|
stack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
1061
|
1101
|
wrap.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
1062
|
1102
|
NSLayoutConstraint.activate([
|
|
1063
|
|
- label.leftAnchor.constraint(equalTo: stack.leftAnchor)
|
|
|
1103
|
+ label.leftAnchor.constraint(equalTo: stack.leftAnchor),
|
|
|
1104
|
+ label.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
1105
|
+ wrap.widthAnchor.constraint(equalTo: stack.widthAnchor)
|
|
1064
|
1106
|
])
|
|
1065
|
1107
|
return stack
|
|
1066
|
1108
|
}
|
|
|
@@ -1210,11 +1252,12 @@ private final class EducationEntryView: NSView {
|
|
1210
|
1252
|
let inner = NSStackView(views: [headerRow, degreeInstitutionRow, yearGroup])
|
|
1211
|
1253
|
inner.orientation = .vertical
|
|
1212
|
1254
|
inner.spacing = 14
|
|
1213
|
|
- inner.alignment = .width
|
|
|
1255
|
+ inner.alignment = .leading
|
|
1214
|
1256
|
inner.translatesAutoresizingMaskIntoConstraints = false
|
|
1215
|
1257
|
inner.edgeInsets = NSEdgeInsets(top: 14, left: 18, bottom: 14, right: 18)
|
|
1216
|
1258
|
inner.userInterfaceLayoutDirection = .leftToRight
|
|
1217
|
1259
|
ProfileLayoutEnforcement.applyForcedLTR(to: inner)
|
|
|
1260
|
+ inner.pinAllArrangedSubviewWidthsEqualToStackWidth()
|
|
1218
|
1261
|
|
|
1219
|
1262
|
addSubview(inner)
|
|
1220
|
1263
|
NSLayoutConstraint.activate([
|