pi-studio 0.9.52 → 0.9.54

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.
@@ -1,11 +1,21 @@
1
1
  (() => {
2
2
  const STUDIO_PREVIEW_LOCAL_IMAGE_LIMIT = 100;
3
+ const STUDIO_PREVIEW_LOCAL_PDF_LIMIT = 40;
3
4
  const STUDIO_PREVIEW_IMAGE_DATA_URL_PATTERN = /^data:image\/(?:png|jpeg|gif|webp);base64,/i;
5
+ const STUDIO_PREVIEW_PDF_DATA_URL_PATTERN = /^data:application\/pdf;base64,/i;
4
6
 
5
7
  function isResolvableStudioPreviewImageSource(value) {
6
8
  const source = String(value || "").trim();
7
9
  if (!source || source.startsWith("#") || source.startsWith("//")) return false;
8
10
  if (/^(?:data|blob|https?|about|javascript):/i.test(source)) return false;
11
+ if (/^file:/i.test(source)) {
12
+ try {
13
+ const fileUrl = new URL(source);
14
+ return fileUrl.protocol === "file:" && (!fileUrl.hostname || fileUrl.hostname === "localhost");
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
9
19
  if (/^[a-z][a-z0-9+.-]*:/i.test(source) && !/^[a-z]:[\\/]/i.test(source)) return false;
10
20
  return true;
11
21
  }
@@ -17,30 +27,66 @@
17
27
  && String(a.resourceDir || "") === String(b.resourceDir || "");
18
28
  }
19
29
 
20
- async function hydrateStudioPreviewLocalImages(target, resolveResource) {
30
+ function isResolvableStudioPreviewPdfSource(value) {
31
+ const source = String(value || "").trim();
32
+ return isResolvableStudioPreviewImageSource(source) && /\.pdf(?:[?#].*)?$/i.test(source);
33
+ }
34
+
35
+ async function hydrateStudioPreviewLocalMediaElements(target, options) {
36
+ const selector = options && options.selector;
37
+ const sourceAllowed = options && options.sourceAllowed;
38
+ const dataUrlAllowed = options && options.dataUrlAllowed;
39
+ const resolveResource = options && options.resolveResource;
40
+ const onError = options && options.onError;
41
+ const limit = Math.max(1, Number(options && options.limit) || 1);
21
42
  if (!target || typeof target.querySelectorAll !== "function" || typeof resolveResource !== "function") {
22
43
  return { attempted: 0, resolved: 0 };
23
44
  }
24
45
 
25
- const images = Array.from(target.querySelectorAll("img[src]"))
26
- .filter((image) => image && typeof image.getAttribute === "function" && isResolvableStudioPreviewImageSource(image.getAttribute("src")))
27
- .slice(0, STUDIO_PREVIEW_LOCAL_IMAGE_LIMIT);
46
+ const elements = Array.from(target.querySelectorAll(selector))
47
+ .filter((element) => element && typeof element.getAttribute === "function" && sourceAllowed(element.getAttribute("src")))
48
+ .slice(0, limit);
28
49
 
29
50
  let resolved = 0;
30
- await Promise.all(images.map(async (image) => {
31
- const originalSource = String(image.getAttribute("src") || "").trim();
51
+ await Promise.all(elements.map(async (element) => {
52
+ const originalSource = String(element.getAttribute("src") || "").trim();
32
53
  try {
33
54
  const dataUrl = await resolveResource(originalSource);
34
- if (!STUDIO_PREVIEW_IMAGE_DATA_URL_PATTERN.test(String(dataUrl || ""))) return;
35
- if (image.isConnected === false || String(image.getAttribute("src") || "").trim() !== originalSource) return;
36
- image.setAttribute("src", dataUrl);
55
+ if (!dataUrlAllowed.test(String(dataUrl || ""))) throw new Error("Studio returned an unsupported local media payload.");
56
+ if (element.isConnected === false || String(element.getAttribute("src") || "").trim() !== originalSource) return;
57
+ element.setAttribute("src", dataUrl);
37
58
  resolved += 1;
38
- } catch {
39
- // Leave unresolved images unchanged so authored browser URLs retain their normal behavior.
59
+ } catch (error) {
60
+ if (element.isConnected === false || String(element.getAttribute("src") || "").trim() !== originalSource) return;
61
+ if (typeof onError === "function") {
62
+ try { await onError(element, originalSource, error); } catch {}
63
+ }
40
64
  }
41
65
  }));
42
66
 
43
- return { attempted: images.length, resolved };
67
+ return { attempted: elements.length, resolved };
68
+ }
69
+
70
+ async function hydrateStudioPreviewLocalImages(target, resolveResource, options) {
71
+ return hydrateStudioPreviewLocalMediaElements(target, {
72
+ selector: "img[src]",
73
+ sourceAllowed: isResolvableStudioPreviewImageSource,
74
+ dataUrlAllowed: STUDIO_PREVIEW_IMAGE_DATA_URL_PATTERN,
75
+ resolveResource,
76
+ onError: options && options.onError,
77
+ limit: STUDIO_PREVIEW_LOCAL_IMAGE_LIMIT,
78
+ });
79
+ }
80
+
81
+ async function hydrateStudioPreviewLocalPdfEmbeds(target, resolveResource, options) {
82
+ return hydrateStudioPreviewLocalMediaElements(target, {
83
+ selector: "embed[src]",
84
+ sourceAllowed: isResolvableStudioPreviewPdfSource,
85
+ dataUrlAllowed: STUDIO_PREVIEW_PDF_DATA_URL_PATTERN,
86
+ resolveResource,
87
+ onError: options && options.onError,
88
+ limit: STUDIO_PREVIEW_LOCAL_PDF_LIMIT,
89
+ });
44
90
  }
45
91
 
46
92
  function buildStudioPdfVersionSignature(headers) {
@@ -94,11 +140,14 @@
94
140
 
95
141
  globalThis.PiStudioPreviewResourceHelpers = Object.freeze({
96
142
  STUDIO_PREVIEW_LOCAL_IMAGE_LIMIT,
143
+ STUDIO_PREVIEW_LOCAL_PDF_LIMIT,
97
144
  areStudioPreviewResourceContextsEqual,
98
145
  buildStudioPdfVersionSignature,
99
146
  createStudioPdfVersionObservationState,
100
147
  hydrateStudioPreviewLocalImages,
148
+ hydrateStudioPreviewLocalPdfEmbeds,
101
149
  isResolvableStudioPreviewImageSource,
150
+ isResolvableStudioPreviewPdfSource,
102
151
  observeStudioPdfVersion,
103
152
  });
104
153
  })();
package/client/studio.css CHANGED
@@ -496,6 +496,45 @@
496
496
  font-size: 14px;
497
497
  }
498
498
 
499
+ body[data-watched-file-preview="1"] #rightSectionHeader .section-header-main::before {
500
+ content: "Watched preview";
501
+ }
502
+
503
+ body[data-watched-file-preview="1"] #saveAsBtn,
504
+ body[data-watched-file-preview="1"] #saveOverBtn,
505
+ body[data-watched-file-preview="1"] #refreshFromDiskBtn,
506
+ body[data-watched-file-preview="1"] #clearWorkspaceBtn,
507
+ body[data-watched-file-preview="1"] #importFileBtn,
508
+ body[data-watched-file-preview="1"] #suggestCompletionBtn,
509
+ body[data-watched-file-preview="1"] #suggestCompletionOptionsBtn,
510
+ body[data-watched-file-preview="1"] #insertHeaderBtn,
511
+ body[data-watched-file-preview="1"] #annotationModeSelect,
512
+ body[data-watched-file-preview="1"] #stripAnnotationsBtn,
513
+ body[data-watched-file-preview="1"] #saveAnnotatedBtn,
514
+ body[data-watched-file-preview="1"] #getEditorBtn {
515
+ display: none !important;
516
+ }
517
+
518
+ body[data-watched-file-preview="1"] #sourceText {
519
+ cursor: default;
520
+ }
521
+
522
+ .studio-watched-preview-error {
523
+ position: sticky;
524
+ top: 8px;
525
+ z-index: 2;
526
+ display: flex;
527
+ align-items: center;
528
+ justify-content: space-between;
529
+ gap: 10px;
530
+ margin: 8px 0;
531
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12);
532
+ }
533
+
534
+ .studio-watched-preview-error button {
535
+ flex: 0 0 auto;
536
+ }
537
+
499
538
  .reference-meta {
500
539
  padding: 8px 10px;
501
540
  border-bottom: 1px solid var(--panel-border);
@@ -3009,6 +3048,17 @@
3009
3048
  font-style: italic;
3010
3049
  }
3011
3050
 
3051
+ .studio-preview-staging {
3052
+ position: fixed !important;
3053
+ top: 0;
3054
+ left: -100000px;
3055
+ z-index: -1;
3056
+ max-height: none !important;
3057
+ overflow: visible !important;
3058
+ visibility: hidden;
3059
+ pointer-events: none;
3060
+ }
3061
+
3012
3062
  .panel-scroll.preview-pending::after {
3013
3063
  content: "Updating";
3014
3064
  position: absolute;
@@ -3032,6 +3082,97 @@
3032
3082
  overflow-anchor: none;
3033
3083
  }
3034
3084
 
3085
+ .rendered-markdown .studio-blocked-media {
3086
+ display: flex;
3087
+ align-items: center;
3088
+ justify-content: space-between;
3089
+ gap: 12px;
3090
+ width: 100%;
3091
+ margin: 10px 0;
3092
+ padding: 10px 12px;
3093
+ border: 1px solid color-mix(in srgb, var(--warn) 42%, var(--panel-border));
3094
+ border-radius: 9px;
3095
+ background: color-mix(in srgb, var(--warn) 7%, var(--panel));
3096
+ color: var(--text);
3097
+ font-family: var(--font-ui);
3098
+ font-size: 12px;
3099
+ font-style: normal;
3100
+ line-height: 1.35;
3101
+ box-sizing: border-box;
3102
+ }
3103
+
3104
+ .rendered-markdown .studio-blocked-media-text {
3105
+ min-width: 0;
3106
+ display: flex;
3107
+ flex-direction: column;
3108
+ gap: 3px;
3109
+ }
3110
+
3111
+ .rendered-markdown .studio-blocked-media-text strong {
3112
+ color: var(--warn);
3113
+ font-weight: 650;
3114
+ }
3115
+
3116
+ .rendered-markdown .studio-blocked-media-text code {
3117
+ display: block;
3118
+ max-width: min(72vw, 760px);
3119
+ overflow: hidden;
3120
+ color: var(--studio-info-text, var(--muted));
3121
+ font-family: var(--font-mono);
3122
+ font-size: 11px;
3123
+ text-overflow: ellipsis;
3124
+ white-space: nowrap;
3125
+ }
3126
+
3127
+ .rendered-markdown button.studio-blocked-media-allow {
3128
+ flex: 0 0 auto;
3129
+ min-height: 30px;
3130
+ padding: 5px 10px;
3131
+ border: 1px solid color-mix(in srgb, var(--warn) 48%, var(--control-border));
3132
+ border-radius: 8px;
3133
+ background: var(--panel);
3134
+ color: var(--text);
3135
+ font: inherit;
3136
+ font-weight: 600;
3137
+ cursor: pointer;
3138
+ white-space: nowrap;
3139
+ }
3140
+
3141
+ .rendered-markdown button.studio-blocked-media-allow:not(:disabled):hover,
3142
+ .rendered-markdown button.studio-blocked-media-allow:focus-visible {
3143
+ border-color: var(--warn);
3144
+ background: var(--control-hover-bg, var(--panel-2));
3145
+ outline: none;
3146
+ }
3147
+
3148
+ .rendered-markdown button.studio-blocked-media-allow:disabled {
3149
+ cursor: default;
3150
+ opacity: 0.6;
3151
+ }
3152
+
3153
+ .rendered-markdown .studio-html-artifact-blocked-media {
3154
+ display: grid;
3155
+ gap: 6px;
3156
+ padding: 0 10px;
3157
+ border-bottom: 1px solid var(--border-subtle);
3158
+ background: var(--panel);
3159
+ }
3160
+
3161
+ .rendered-markdown .studio-html-artifact-blocked-media .studio-blocked-media {
3162
+ margin: 8px 0;
3163
+ }
3164
+
3165
+ @media (max-width: 680px) {
3166
+ .rendered-markdown .studio-blocked-media {
3167
+ align-items: stretch;
3168
+ flex-direction: column;
3169
+ }
3170
+
3171
+ .rendered-markdown button.studio-blocked-media-allow {
3172
+ align-self: flex-start;
3173
+ }
3174
+ }
3175
+
3035
3176
  .preview-error {
3036
3177
  color: var(--warn);
3037
3178
  margin-bottom: 0.75em;
@@ -3701,13 +3842,15 @@
3701
3842
 
3702
3843
  .files-toolbar-actions button,
3703
3844
  .files-actions button,
3704
- .files-sort-select {
3845
+ .files-sort-select,
3846
+ .files-location-select {
3705
3847
  padding: 4px 7px;
3706
3848
  font-size: 11px;
3707
3849
  }
3708
3850
 
3709
- .files-sort-select {
3710
- max-width: 150px;
3851
+ .files-sort-select,
3852
+ .files-location-select {
3853
+ max-width: 240px;
3711
3854
  border: 1px solid var(--control-border);
3712
3855
  border-radius: 7px;
3713
3856
  background: var(--panel);
@@ -3715,6 +3858,26 @@
3715
3858
  font-family: var(--font-ui);
3716
3859
  }
3717
3860
 
3861
+ .files-sort-select {
3862
+ max-width: 150px;
3863
+ }
3864
+
3865
+ .files-no-locations,
3866
+ .files-section-title {
3867
+ color: var(--studio-info-text, var(--muted));
3868
+ font-size: 11px;
3869
+ font-weight: 700;
3870
+ letter-spacing: 0.03em;
3871
+ text-transform: uppercase;
3872
+ }
3873
+
3874
+ .files-exact-section {
3875
+ display: grid;
3876
+ gap: 6px;
3877
+ padding-bottom: 10px;
3878
+ border-bottom: 1px solid var(--border-subtle);
3879
+ }
3880
+
3718
3881
  .files-subtitle,
3719
3882
  .files-notice,
3720
3883
  .files-empty {
@@ -6317,6 +6480,13 @@
6317
6480
  font-weight: 650;
6318
6481
  }
6319
6482
 
6483
+ .studio-decision-confirm:not(.is-destructive):not(:disabled):hover,
6484
+ .studio-decision-confirm:not(.is-destructive):focus-visible {
6485
+ border-color: color-mix(in srgb, var(--accent) 84%, var(--text));
6486
+ background: color-mix(in srgb, var(--accent) 84%, var(--text));
6487
+ color: var(--accent-contrast);
6488
+ }
6489
+
6320
6490
  .studio-decision-confirm.is-destructive {
6321
6491
  border-color: var(--warn-border);
6322
6492
  background: color-mix(in srgb, var(--warn) 15%, var(--panel));
@@ -6435,6 +6605,13 @@
6435
6605
  margin: 6px 0 0 18px;
6436
6606
  }
6437
6607
 
6608
+ .side-question-context-access-note {
6609
+ margin: 9px 0 0;
6610
+ color: var(--muted);
6611
+ font-size: 0.78rem;
6612
+ line-height: 1.42;
6613
+ }
6614
+
6438
6615
  .side-question-context-grid select,
6439
6616
  .side-question-path-label input,
6440
6617
  .side-question-composer-label textarea {