pi-studio 0.9.52 → 0.9.53

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
@@ -3032,6 +3032,97 @@
3032
3032
  overflow-anchor: none;
3033
3033
  }
3034
3034
 
3035
+ .rendered-markdown .studio-blocked-media {
3036
+ display: flex;
3037
+ align-items: center;
3038
+ justify-content: space-between;
3039
+ gap: 12px;
3040
+ width: 100%;
3041
+ margin: 10px 0;
3042
+ padding: 10px 12px;
3043
+ border: 1px solid color-mix(in srgb, var(--warn) 42%, var(--panel-border));
3044
+ border-radius: 9px;
3045
+ background: color-mix(in srgb, var(--warn) 7%, var(--panel));
3046
+ color: var(--text);
3047
+ font-family: var(--font-ui);
3048
+ font-size: 12px;
3049
+ font-style: normal;
3050
+ line-height: 1.35;
3051
+ box-sizing: border-box;
3052
+ }
3053
+
3054
+ .rendered-markdown .studio-blocked-media-text {
3055
+ min-width: 0;
3056
+ display: flex;
3057
+ flex-direction: column;
3058
+ gap: 3px;
3059
+ }
3060
+
3061
+ .rendered-markdown .studio-blocked-media-text strong {
3062
+ color: var(--warn);
3063
+ font-weight: 650;
3064
+ }
3065
+
3066
+ .rendered-markdown .studio-blocked-media-text code {
3067
+ display: block;
3068
+ max-width: min(72vw, 760px);
3069
+ overflow: hidden;
3070
+ color: var(--studio-info-text, var(--muted));
3071
+ font-family: var(--font-mono);
3072
+ font-size: 11px;
3073
+ text-overflow: ellipsis;
3074
+ white-space: nowrap;
3075
+ }
3076
+
3077
+ .rendered-markdown button.studio-blocked-media-allow {
3078
+ flex: 0 0 auto;
3079
+ min-height: 30px;
3080
+ padding: 5px 10px;
3081
+ border: 1px solid color-mix(in srgb, var(--warn) 48%, var(--control-border));
3082
+ border-radius: 8px;
3083
+ background: var(--panel);
3084
+ color: var(--text);
3085
+ font: inherit;
3086
+ font-weight: 600;
3087
+ cursor: pointer;
3088
+ white-space: nowrap;
3089
+ }
3090
+
3091
+ .rendered-markdown button.studio-blocked-media-allow:not(:disabled):hover,
3092
+ .rendered-markdown button.studio-blocked-media-allow:focus-visible {
3093
+ border-color: var(--warn);
3094
+ background: var(--control-hover-bg, var(--panel-2));
3095
+ outline: none;
3096
+ }
3097
+
3098
+ .rendered-markdown button.studio-blocked-media-allow:disabled {
3099
+ cursor: default;
3100
+ opacity: 0.6;
3101
+ }
3102
+
3103
+ .rendered-markdown .studio-html-artifact-blocked-media {
3104
+ display: grid;
3105
+ gap: 6px;
3106
+ padding: 0 10px;
3107
+ border-bottom: 1px solid var(--border-subtle);
3108
+ background: var(--panel);
3109
+ }
3110
+
3111
+ .rendered-markdown .studio-html-artifact-blocked-media .studio-blocked-media {
3112
+ margin: 8px 0;
3113
+ }
3114
+
3115
+ @media (max-width: 680px) {
3116
+ .rendered-markdown .studio-blocked-media {
3117
+ align-items: stretch;
3118
+ flex-direction: column;
3119
+ }
3120
+
3121
+ .rendered-markdown button.studio-blocked-media-allow {
3122
+ align-self: flex-start;
3123
+ }
3124
+ }
3125
+
3035
3126
  .preview-error {
3036
3127
  color: var(--warn);
3037
3128
  margin-bottom: 0.75em;
@@ -3701,13 +3792,15 @@
3701
3792
 
3702
3793
  .files-toolbar-actions button,
3703
3794
  .files-actions button,
3704
- .files-sort-select {
3795
+ .files-sort-select,
3796
+ .files-location-select {
3705
3797
  padding: 4px 7px;
3706
3798
  font-size: 11px;
3707
3799
  }
3708
3800
 
3709
- .files-sort-select {
3710
- max-width: 150px;
3801
+ .files-sort-select,
3802
+ .files-location-select {
3803
+ max-width: 240px;
3711
3804
  border: 1px solid var(--control-border);
3712
3805
  border-radius: 7px;
3713
3806
  background: var(--panel);
@@ -3715,6 +3808,26 @@
3715
3808
  font-family: var(--font-ui);
3716
3809
  }
3717
3810
 
3811
+ .files-sort-select {
3812
+ max-width: 150px;
3813
+ }
3814
+
3815
+ .files-no-locations,
3816
+ .files-section-title {
3817
+ color: var(--studio-info-text, var(--muted));
3818
+ font-size: 11px;
3819
+ font-weight: 700;
3820
+ letter-spacing: 0.03em;
3821
+ text-transform: uppercase;
3822
+ }
3823
+
3824
+ .files-exact-section {
3825
+ display: grid;
3826
+ gap: 6px;
3827
+ padding-bottom: 10px;
3828
+ border-bottom: 1px solid var(--border-subtle);
3829
+ }
3830
+
3718
3831
  .files-subtitle,
3719
3832
  .files-notice,
3720
3833
  .files-empty {
@@ -6317,6 +6430,13 @@
6317
6430
  font-weight: 650;
6318
6431
  }
6319
6432
 
6433
+ .studio-decision-confirm:not(.is-destructive):not(:disabled):hover,
6434
+ .studio-decision-confirm:not(.is-destructive):focus-visible {
6435
+ border-color: color-mix(in srgb, var(--accent) 84%, var(--text));
6436
+ background: color-mix(in srgb, var(--accent) 84%, var(--text));
6437
+ color: var(--accent-contrast);
6438
+ }
6439
+
6320
6440
  .studio-decision-confirm.is-destructive {
6321
6441
  border-color: var(--warn-border);
6322
6442
  background: color-mix(in srgb, var(--warn) 15%, var(--panel));
@@ -6435,6 +6555,13 @@
6435
6555
  margin: 6px 0 0 18px;
6436
6556
  }
6437
6557
 
6558
+ .side-question-context-access-note {
6559
+ margin: 9px 0 0;
6560
+ color: var(--muted);
6561
+ font-size: 0.78rem;
6562
+ line-height: 1.42;
6563
+ }
6564
+
6438
6565
  .side-question-context-grid select,
6439
6566
  .side-question-path-label input,
6440
6567
  .side-question-composer-label textarea {