@colixsystems/widget-sdk 0.186.0 → 0.188.0
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.
- package/README.md +70 -2
- package/dist/contract.cjs +61 -5
- package/dist/contract.js +61 -5
- package/dist/hooks.js +112 -10
- package/dist/index.d.ts +56 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/linter.cjs +85 -1
- package/dist/linter.js +85 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,8 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
20
20
|
| **CORE** | `useTheme()` | `{ colors, elevation, interaction, spacing, spacingScale, radii, typography, components, widgetStyles }` | `ctx.workspace.theme` — no scope. `elevation` is the shared depth scale (`none / sm / md / lg / xl`) you spread into a style; `colors` includes the accent's quiet tiers (`primarySoft` / `onPrimarySoft` / `primaryStrong`) and `loader`, the spinner colour for your own loading state. `components` is HOST-OWNED (the theme's per-component style tokens); the host has already folded it into your `props.style`, so read `useWidgetStyle()` and ignore this slice. |
|
|
21
21
|
| **CORE** | `useWorkspaceCurrency()` | `{ currency, formatMoney }` | `ctx.workspace.currency` — no scope. The currency this workspace charges its app users in, resolved at RENDER time. Render every price as `formatMoney(minorUnits)` and never write a currency symbol or code into a widget: the owner can change it after the widget ships, and a baked label then contradicts the charge. |
|
|
22
22
|
| **CORE** | `useWidgetStyle()` | `{ [styleField]: value }` | `ctx.props.style` — no scope. The author-set per-widget style values declared in `manifest.styleSchema`; apply each onto whatever element you choose. |
|
|
23
|
-
| **CORE** | `useUser()` | `{ id, email, displayName, roles, groupIds }` | `ctx.user` (host-built context, **camelCase** — not a wire payload; `id` null when anonymous) — no scope |
|
|
23
|
+
| **CORE** | `useUser()` | `{ id, email, displayName, photoUrl, roles, groupIds }` | `ctx.user` (host-built context, **camelCase** — not a wire payload; `id` null when anonymous) — no scope. `photoUrl` is the user's own profile photo as an absolute URL, or `null`. |
|
|
24
|
+
| **CORE** | `useUserPhoto()` | `{ url, canEdit, upload, remove, uploading, error }` | `ctx.user` + the optional `ctx.account` — no scope. Changes the signed-in user's OWN profile photo. `canEdit` is false when nobody is signed in or the host cannot change it; `upload` / `remove` then reject. |
|
|
24
25
|
| **CORE** | `useNavigation()` | `{ goTo, goBack, push, replace, back, currentRoute, openLink }` | `ctx.navigation` — no scope. `goTo(pageIdOrSlug, params?)` accepts a page UUID (a `pageRef` prop) **or its slug** (a row's page-key field) — both hosts resolve either (`openLink` for a link of unknown shape; a known external URL can also use the `Linking` primitive) |
|
|
25
26
|
| **CORE** | `useRouteParams()` | `{ [paramKey]: value }` | `ctx.navigation.currentRoute.params` — no scope. The nav params the previous page passed via `goTo(pageId, params)`; the flat accessor for master→detail (read `recordId` on a detail page). Empty object when none. |
|
|
26
27
|
| **CORE** | `usePageContext()` | `{ params, records }` | `ctx.pageContext` — no scope. The page's DECLARED parameters, resolved once by the host: `params` are coerced to their declared types, `records` holds the row already fetched for each `record` param (read it instead of fetching again). Both empty when the page declares none. |
|
|
@@ -74,8 +75,75 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
74
75
|
|
|
75
76
|
## Status
|
|
76
77
|
|
|
77
|
-
`v0.
|
|
78
|
+
`v0.188.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
78
79
|
|
|
80
|
+
### What's new in 0.188.0 (contract 1.149.0)
|
|
81
|
+
|
|
82
|
+
**New linter rules for Leaflet maps: `leaflet-css-missing` and `leaflet-default-marker-icon`.** A widget's web entry is bundled with its own copy of Leaflet, and nothing else loads Leaflet's stylesheet. A map that skipped `import "leaflet/dist/leaflet.css"` drew a small patch of tiles inside an otherwise empty box, with its panes out of place and its markers invisible. Even with the stylesheet, Leaflet looks up its default marker image relative to the page, which 404s inside a widget, so a `<Marker>` with no `icon` showed no pin.
|
|
83
|
+
|
|
84
|
+
- **`leaflet-css-missing` (severity `error`).** A file that imports `leaflet` or `react-leaflet` code (an image-only import does not count) must also import `"leaflet/dist/leaflet.css"`. The bundler turns that import into an injected `<style>`. Import it in every file that uses Leaflet; the bundler includes it once.
|
|
85
|
+
- **`leaflet-default-marker-icon` (severity `warning`).** A react-leaflet `<Marker>` with no `icon` prop. Import the marker PNGs (the bundler inlines them as data URLs), build the icon once at module scope and pass it in:
|
|
86
|
+
|
|
87
|
+
```jsx
|
|
88
|
+
import L from "leaflet";
|
|
89
|
+
import "leaflet/dist/leaflet.css";
|
|
90
|
+
import markerIcon from "leaflet/dist/images/marker-icon.png";
|
|
91
|
+
import markerIcon2x from "leaflet/dist/images/marker-icon-2x.png";
|
|
92
|
+
import markerShadow from "leaflet/dist/images/marker-shadow.png";
|
|
93
|
+
|
|
94
|
+
const pin = L.icon({
|
|
95
|
+
iconUrl: markerIcon, iconRetinaUrl: markerIcon2x, shadowUrl: markerShadow,
|
|
96
|
+
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41],
|
|
97
|
+
});
|
|
98
|
+
// <Marker position={[lat, lng]} icon={pin} />
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
It is a warning because a widget that configures `L.Icon.Default` itself is fine, and is not flagged. `<CircleMarker>` needs no icon.
|
|
102
|
+
|
|
103
|
+
The `leaflet` and `react-leaflet` entries in `CONTRACT.vettedImports` now say the same. Comments are not scanned, so documenting the pattern is safe.
|
|
104
|
+
|
|
105
|
+
### What's new in 0.187.0 (contract 1.148.0)
|
|
106
|
+
|
|
107
|
+
**App users have a profile photo, and a widget can show and change it.** `useUser()` now returns `photoUrl`, and the new `useUserPhoto()` hook lets the signed-in person replace or remove their own picture:
|
|
108
|
+
|
|
109
|
+
```jsx
|
|
110
|
+
import { useUser, useUserPhoto, Avatar, FilePicker, Pressable, Text, View } from "@colixsystems/widget-sdk";
|
|
111
|
+
|
|
112
|
+
function ProfileHeader() {
|
|
113
|
+
const user = useUser();
|
|
114
|
+
const { canEdit, upload, remove, uploading, error } = useUserPhoto();
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<View>
|
|
118
|
+
<Avatar name={user.displayName} url={user.photoUrl} size="xl" />
|
|
119
|
+
{canEdit ? (
|
|
120
|
+
<FilePicker
|
|
121
|
+
accept="image/png,image/jpeg,image/webp"
|
|
122
|
+
disabled={uploading}
|
|
123
|
+
onPick={(file) => upload(file).catch(() => {})}
|
|
124
|
+
>
|
|
125
|
+
<Text>{uploading ? "Saving…" : "Change photo"}</Text>
|
|
126
|
+
</FilePicker>
|
|
127
|
+
) : null}
|
|
128
|
+
{canEdit && user.photoUrl ? (
|
|
129
|
+
<Pressable disabled={uploading} onPress={() => remove().catch(() => {})}>
|
|
130
|
+
<Text>Remove photo</Text>
|
|
131
|
+
</Pressable>
|
|
132
|
+
) : null}
|
|
133
|
+
{error ? <Text>{error.status === 413 ? "That image is too large." : "Could not save the photo."}</Text> : null}
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- **`useUser().photoUrl`** is an absolute URL you pass straight to `<Avatar url>` or `<Image source={{ uri }}>`. It is `null` when no photo is set, for anonymous visitors, and on a host that does not provide one. `<Avatar>` falls back to the name's initials, so the slot is never blank.
|
|
140
|
+
- **`useUserPhoto()`** returns `{ url, canEdit, upload, remove, uploading, error }`. `url` is the same value as `useUser().photoUrl`. `upload(file)` takes the file `<FilePicker>` gives you, or `asset.file` from `useCamera()` / `useImageEditor()` — never the asset itself (a browser `File` on web, expo-file-system's `File` in the exported app) and resolves `{ photoUrl }`; `remove()` resolves `{ photoUrl: null }`. Once either resolves, every widget on the page reads the new `useUser().photoUrl`.
|
|
141
|
+
- **The server crops the image to a square and resizes it.** PNG, JPEG or WebP, 10 MB at most. A refusal carries `err.status`: `413` too large, `415` unsupported type, `400` unreadable image.
|
|
142
|
+
- **Check `canEdit` before offering the control.** It is false when nobody is signed in and on a host that cannot change the photo, such as the Builder canvas preview. `upload()` and `remove()` then reject (`"Sign in to change your photo"` / `"Changing your photo is not available here"`) and set `error`; the hook itself never throws at render.
|
|
143
|
+
- **Only the signed-in user's own photo.** There is no way to read another user's photo: `useUsers()` and `useDirectory()` rows do not carry one.
|
|
144
|
+
- **No new scope.** The hook only ever touches the caller's own account.
|
|
145
|
+
|
|
146
|
+
A host backs the hook with the new optional `WidgetContext.account` slice, `{ setPhoto(file), removePhoto() }`. A host without it needs no change: `photoUrl` reads `null` and `canEdit` is false.
|
|
79
147
|
### What's new in 0.186.0 (contract 1.147.0)
|
|
80
148
|
|
|
81
149
|
**A page's top navigation bar puts its tabs on the left or the right; the centred option is gone.** `resolveTopBarTokens().tabAlignment` is now `"left"` | `"right"`. A `theme_config.topBar.tabAlignment` of `"center"` resolves to `"left"`, like any other unknown value, and the Studio drops it the next time the theme is saved. Host-integration surface only — no author-facing hook, prop, primitive, or manifest field changed.
|
package/dist/contract.cjs
CHANGED
|
@@ -808,12 +808,39 @@ const HOOKS = [
|
|
|
808
808
|
id: "string | null",
|
|
809
809
|
email: "string | null",
|
|
810
810
|
displayName: "string | null",
|
|
811
|
+
photoUrl:
|
|
812
|
+
"string | null // the signed-in user's own profile photo, an absolute URL ready for <Avatar url> or <Image source={{ uri }}>; null when none is set, for anonymous visitors, and on a host that does not provide it",
|
|
811
813
|
roles: "string[]",
|
|
812
814
|
groupIds: "string[]",
|
|
813
815
|
},
|
|
814
816
|
requiredContextSlice: ["user"],
|
|
815
817
|
scopes: null,
|
|
816
818
|
},
|
|
819
|
+
{
|
|
820
|
+
name: "useUserPhoto",
|
|
821
|
+
signature: "useUserPhoto()",
|
|
822
|
+
description:
|
|
823
|
+
"Read and change the signed-in user's OWN profile photo. url mirrors useUser().photoUrl. upload(file) takes a browser " +
|
|
824
|
+
"File on web and expo-file-system's File on native: the file <FilePicker> gives you, or asset.file from useCamera() / " +
|
|
825
|
+
"useImageEditor() — NEVER the asset itself — and the host sends it; never build a FormData or call an endpoint yourself. The server crops " +
|
|
826
|
+
"the image to a square and resizes it. PNG, JPEG or WebP, 10 MB at most; a refusal carries err.status: 413 too large, " +
|
|
827
|
+
"415 unsupported type, 400 unreadable image. remove() clears the photo. After either resolves, useUser().photoUrl and " +
|
|
828
|
+
"url update in every widget on the page. canEdit is false when nobody is signed in and on a host that cannot change " +
|
|
829
|
+
"the photo (the Builder canvas preview): hide or disable the control then — upload() / remove() reject with an Error and " +
|
|
830
|
+
"set error rather than the hook throwing at render, as does upload() with no file. uploading is true while any call is in flight; error holds the last " +
|
|
831
|
+
"failure and clears on the next attempt. Needs no scope: it only ever touches the caller's own account. There is NO way " +
|
|
832
|
+
"to read another user's photo — useUsers() and useDirectory() rows carry none. Identical on web (Player) and the Expo export.",
|
|
833
|
+
returnShape: {
|
|
834
|
+
url: "string | null // same value as useUser().photoUrl",
|
|
835
|
+
canEdit: "boolean // signed in AND the host can change the photo",
|
|
836
|
+
upload: "(file: File) => Promise<{ photoUrl: string | null }>",
|
|
837
|
+
remove: "() => Promise<{ photoUrl: null }>",
|
|
838
|
+
uploading: "boolean",
|
|
839
|
+
error: "Error | null",
|
|
840
|
+
},
|
|
841
|
+
requiredContextSlice: ["user", "account.setPhoto", "account.removePhoto"],
|
|
842
|
+
scopes: null,
|
|
843
|
+
},
|
|
817
844
|
{
|
|
818
845
|
name: "useFill",
|
|
819
846
|
signature: "useFill()",
|
|
@@ -2792,9 +2819,26 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2792
2819
|
},
|
|
2793
2820
|
user: {
|
|
2794
2821
|
description:
|
|
2795
|
-
"Signed-in user, host-built context object (camelCase: { id, email, displayName, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. Not a data-client.",
|
|
2822
|
+
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. Not a data-client.",
|
|
2796
2823
|
required: true,
|
|
2797
|
-
fields: {
|
|
2824
|
+
fields: {
|
|
2825
|
+
id: "string",
|
|
2826
|
+
email: "string",
|
|
2827
|
+
displayName: "string",
|
|
2828
|
+
photoUrl: "string | null",
|
|
2829
|
+
},
|
|
2830
|
+
},
|
|
2831
|
+
// Optional so a host that cannot change the photo needs no stub:
|
|
2832
|
+
// useUserPhoto() reports canEdit false when it is absent.
|
|
2833
|
+
account: {
|
|
2834
|
+
description:
|
|
2835
|
+
"Optional self-service account actions for the signed-in user: " +
|
|
2836
|
+
"{ setPhoto(file) -> Promise<{ photoUrl }>, removePhoto() -> Promise<{ photoUrl: null }> }. " +
|
|
2837
|
+
"Backs useUserPhoto(). The host performs the request AND updates its own session, so user.photoUrl changes for every " +
|
|
2838
|
+
"widget. setPhoto takes a browser File on web and expo-file-system's File on native. Absent on a host that cannot " +
|
|
2839
|
+
"change the photo (the Builder canvas preview), where useUserPhoto() degrades to canEdit: false rather than throwing.",
|
|
2840
|
+
required: false,
|
|
2841
|
+
fields: { setPhoto: "function", removePhoto: "function" },
|
|
2798
2842
|
},
|
|
2799
2843
|
workspace: {
|
|
2800
2844
|
description:
|
|
@@ -3252,13 +3296,16 @@ const VETTED_IMPORTS = [
|
|
|
3252
3296
|
platforms: ["web"],
|
|
3253
3297
|
category: "geo",
|
|
3254
3298
|
description:
|
|
3255
|
-
"Web-only mapping library. Use alongside react-leaflet in widget.web.jsx as the web counterpart to react-native-maps."
|
|
3299
|
+
"Web-only mapping library. Use alongside react-leaflet in widget.web.jsx as the web counterpart to react-native-maps. " +
|
|
3300
|
+
'Every file that imports it must also `import "leaflet/dist/leaflet.css"` (the linter enforces it), and a marker needs an ' +
|
|
3301
|
+
"explicit `L.icon` built from the imported `leaflet/dist/images/*.png`: Leaflet's default icon cannot resolve its image inside a bundle.",
|
|
3256
3302
|
},
|
|
3257
3303
|
{
|
|
3258
3304
|
specifier: "react-leaflet",
|
|
3259
3305
|
platforms: ["web"],
|
|
3260
3306
|
category: "geo",
|
|
3261
|
-
description:
|
|
3307
|
+
description:
|
|
3308
|
+
'React bindings for leaflet. Web-only. Import "leaflet/dist/leaflet.css" alongside it and pass `icon` to every <Marker>.',
|
|
3262
3309
|
},
|
|
3263
3310
|
{
|
|
3264
3311
|
specifier: "expo-audio",
|
|
@@ -4815,7 +4862,16 @@ const CONTRACT = deepFreeze({
|
|
|
4815
4862
|
// icon, figure and delta wrap in a tile too narrow to hold them.
|
|
4816
4863
|
// 1.147.0: narrowing (sc-10123) — `resolveTopBarTokens().tabAlignment` is
|
|
4817
4864
|
// "left" | "right". A stored "center" resolves to "left".
|
|
4818
|
-
|
|
4865
|
+
// 1.148.0: additive (sc-10142) — `useUser()` returns `photoUrl`, the
|
|
4866
|
+
// signed-in user's own profile photo (null when unset or not provided).
|
|
4867
|
+
// New `useUserPhoto()` changes it through the new OPTIONAL context
|
|
4868
|
+
// slice `account` ({ setPhoto, removePhoto }); a host without it reads
|
|
4869
|
+
// `canEdit: false`.
|
|
4870
|
+
// 1.149.0: narrowing (sc-10466) — new linter rule `leaflet-css-missing`
|
|
4871
|
+
// (error): a file importing leaflet / react-leaflet must import
|
|
4872
|
+
// "leaflet/dist/leaflet.css". `leaflet-default-marker-icon` (warning)
|
|
4873
|
+
// flags a react-leaflet <Marker> with no `icon`.
|
|
4874
|
+
version: "1.149.0",
|
|
4819
4875
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4820
4876
|
hooks: HOOKS,
|
|
4821
4877
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -808,12 +808,39 @@ const HOOKS = [
|
|
|
808
808
|
id: "string | null",
|
|
809
809
|
email: "string | null",
|
|
810
810
|
displayName: "string | null",
|
|
811
|
+
photoUrl:
|
|
812
|
+
"string | null // the signed-in user's own profile photo, an absolute URL ready for <Avatar url> or <Image source={{ uri }}>; null when none is set, for anonymous visitors, and on a host that does not provide it",
|
|
811
813
|
roles: "string[]",
|
|
812
814
|
groupIds: "string[]",
|
|
813
815
|
},
|
|
814
816
|
requiredContextSlice: ["user"],
|
|
815
817
|
scopes: null,
|
|
816
818
|
},
|
|
819
|
+
{
|
|
820
|
+
name: "useUserPhoto",
|
|
821
|
+
signature: "useUserPhoto()",
|
|
822
|
+
description:
|
|
823
|
+
"Read and change the signed-in user's OWN profile photo. url mirrors useUser().photoUrl. upload(file) takes a browser " +
|
|
824
|
+
"File on web and expo-file-system's File on native: the file <FilePicker> gives you, or asset.file from useCamera() / " +
|
|
825
|
+
"useImageEditor() — NEVER the asset itself — and the host sends it; never build a FormData or call an endpoint yourself. The server crops " +
|
|
826
|
+
"the image to a square and resizes it. PNG, JPEG or WebP, 10 MB at most; a refusal carries err.status: 413 too large, " +
|
|
827
|
+
"415 unsupported type, 400 unreadable image. remove() clears the photo. After either resolves, useUser().photoUrl and " +
|
|
828
|
+
"url update in every widget on the page. canEdit is false when nobody is signed in and on a host that cannot change " +
|
|
829
|
+
"the photo (the Builder canvas preview): hide or disable the control then — upload() / remove() reject with an Error and " +
|
|
830
|
+
"set error rather than the hook throwing at render, as does upload() with no file. uploading is true while any call is in flight; error holds the last " +
|
|
831
|
+
"failure and clears on the next attempt. Needs no scope: it only ever touches the caller's own account. There is NO way " +
|
|
832
|
+
"to read another user's photo — useUsers() and useDirectory() rows carry none. Identical on web (Player) and the Expo export.",
|
|
833
|
+
returnShape: {
|
|
834
|
+
url: "string | null // same value as useUser().photoUrl",
|
|
835
|
+
canEdit: "boolean // signed in AND the host can change the photo",
|
|
836
|
+
upload: "(file: File) => Promise<{ photoUrl: string | null }>",
|
|
837
|
+
remove: "() => Promise<{ photoUrl: null }>",
|
|
838
|
+
uploading: "boolean",
|
|
839
|
+
error: "Error | null",
|
|
840
|
+
},
|
|
841
|
+
requiredContextSlice: ["user", "account.setPhoto", "account.removePhoto"],
|
|
842
|
+
scopes: null,
|
|
843
|
+
},
|
|
817
844
|
{
|
|
818
845
|
name: "useFill",
|
|
819
846
|
signature: "useFill()",
|
|
@@ -2788,9 +2815,26 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
2788
2815
|
},
|
|
2789
2816
|
user: {
|
|
2790
2817
|
description:
|
|
2791
|
-
"Signed-in user, host-built context object (camelCase: { id, email, displayName, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. Not a data-client.",
|
|
2818
|
+
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. Not a data-client.",
|
|
2792
2819
|
required: true,
|
|
2793
|
-
fields: {
|
|
2820
|
+
fields: {
|
|
2821
|
+
id: "string",
|
|
2822
|
+
email: "string",
|
|
2823
|
+
displayName: "string",
|
|
2824
|
+
photoUrl: "string | null",
|
|
2825
|
+
},
|
|
2826
|
+
},
|
|
2827
|
+
// Optional so a host that cannot change the photo needs no stub:
|
|
2828
|
+
// useUserPhoto() reports canEdit false when it is absent.
|
|
2829
|
+
account: {
|
|
2830
|
+
description:
|
|
2831
|
+
"Optional self-service account actions for the signed-in user: " +
|
|
2832
|
+
"{ setPhoto(file) -> Promise<{ photoUrl }>, removePhoto() -> Promise<{ photoUrl: null }> }. " +
|
|
2833
|
+
"Backs useUserPhoto(). The host performs the request AND updates its own session, so user.photoUrl changes for every " +
|
|
2834
|
+
"widget. setPhoto takes a browser File on web and expo-file-system's File on native. Absent on a host that cannot " +
|
|
2835
|
+
"change the photo (the Builder canvas preview), where useUserPhoto() degrades to canEdit: false rather than throwing.",
|
|
2836
|
+
required: false,
|
|
2837
|
+
fields: { setPhoto: "function", removePhoto: "function" },
|
|
2794
2838
|
},
|
|
2795
2839
|
workspace: {
|
|
2796
2840
|
description:
|
|
@@ -3248,13 +3292,16 @@ const VETTED_IMPORTS = [
|
|
|
3248
3292
|
platforms: ["web"],
|
|
3249
3293
|
category: "geo",
|
|
3250
3294
|
description:
|
|
3251
|
-
"Web-only mapping library. Use alongside react-leaflet in widget.web.jsx as the web counterpart to react-native-maps."
|
|
3295
|
+
"Web-only mapping library. Use alongside react-leaflet in widget.web.jsx as the web counterpart to react-native-maps. " +
|
|
3296
|
+
'Every file that imports it must also `import "leaflet/dist/leaflet.css"` (the linter enforces it), and a marker needs an ' +
|
|
3297
|
+
"explicit `L.icon` built from the imported `leaflet/dist/images/*.png`: Leaflet's default icon cannot resolve its image inside a bundle.",
|
|
3252
3298
|
},
|
|
3253
3299
|
{
|
|
3254
3300
|
specifier: "react-leaflet",
|
|
3255
3301
|
platforms: ["web"],
|
|
3256
3302
|
category: "geo",
|
|
3257
|
-
description:
|
|
3303
|
+
description:
|
|
3304
|
+
'React bindings for leaflet. Web-only. Import "leaflet/dist/leaflet.css" alongside it and pass `icon` to every <Marker>.',
|
|
3258
3305
|
},
|
|
3259
3306
|
{
|
|
3260
3307
|
specifier: "expo-audio",
|
|
@@ -4811,7 +4858,16 @@ const CONTRACT = deepFreeze({
|
|
|
4811
4858
|
// icon, figure and delta wrap in a tile too narrow to hold them.
|
|
4812
4859
|
// 1.147.0: narrowing (sc-10123) — `resolveTopBarTokens().tabAlignment` is
|
|
4813
4860
|
// "left" | "right". A stored "center" resolves to "left".
|
|
4814
|
-
|
|
4861
|
+
// 1.148.0: additive (sc-10142) — `useUser()` returns `photoUrl`, the
|
|
4862
|
+
// signed-in user's own profile photo (null when unset or not provided).
|
|
4863
|
+
// New `useUserPhoto()` changes it through the new OPTIONAL context
|
|
4864
|
+
// slice `account` ({ setPhoto, removePhoto }); a host without it reads
|
|
4865
|
+
// `canEdit: false`.
|
|
4866
|
+
// 1.149.0: narrowing (sc-10466) — new linter rule `leaflet-css-missing`
|
|
4867
|
+
// (error): a file importing leaflet / react-leaflet must import
|
|
4868
|
+
// "leaflet/dist/leaflet.css". `leaflet-default-marker-icon` (warning)
|
|
4869
|
+
// flags a react-leaflet <Marker> with no `icon`.
|
|
4870
|
+
version: "1.149.0",
|
|
4815
4871
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4816
4872
|
hooks: HOOKS,
|
|
4817
4873
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -242,8 +242,10 @@ export function useWidgetStyle() {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
|
-
* Returns the active end-user identity
|
|
246
|
-
* `{ id, email, displayName, roles, groupIds }` (camelCase — host-built context, not a wire payload).
|
|
245
|
+
* Returns the active end-user identity the host provides, e.g.
|
|
246
|
+
* `{ id, email, displayName, photoUrl, roles, groupIds }` (camelCase — host-built context, not a wire payload).
|
|
247
|
+
* It is the host's own object, or a memoised copy carrying `photoUrl: null`
|
|
248
|
+
* when the host omits that field.
|
|
247
249
|
*
|
|
248
250
|
* `id` is `null` for anonymous visitors (and on the Studio canvas preview,
|
|
249
251
|
* which renders widgets as if signed-out so the public branch shows). The
|
|
@@ -251,12 +253,112 @@ export function useWidgetStyle() {
|
|
|
251
253
|
* fields; widgets read them directly. The current principal is host-provided
|
|
252
254
|
* — this hook does NOT read a data-client.
|
|
253
255
|
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
+
* `photoUrl` is the user's own profile photo as an absolute URL, ready for
|
|
257
|
+
* `<Avatar url>` or `<Image source={{ uri }}>`; `null` when none is set, for
|
|
258
|
+
* anonymous visitors, and on a host that does not provide it.
|
|
259
|
+
*
|
|
260
|
+
* Use this to render the signed-in user's name and photo in a header, branch
|
|
261
|
+
* on roles, or stamp a created-by field.
|
|
256
262
|
*/
|
|
257
263
|
export function useUser() {
|
|
258
264
|
const ctx = useWidgetContextOrThrow("useUser");
|
|
259
|
-
|
|
265
|
+
const user = ctx.user;
|
|
266
|
+
// A host predating photoUrl omits it; the type promises null.
|
|
267
|
+
return useMemo(
|
|
268
|
+
() => (user && user.photoUrl === undefined ? { ...user, photoUrl: null } : user),
|
|
269
|
+
[user],
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const USER_PHOTO_UNAVAILABLE = "Changing your photo is not available here";
|
|
274
|
+
const USER_PHOTO_SIGNED_OUT = "Sign in to change your photo";
|
|
275
|
+
const USER_PHOTO_NO_FILE = "useUserPhoto: file is required";
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Read and change the signed-in user's OWN profile photo. Returns
|
|
279
|
+
* `{ url, canEdit, upload, remove, uploading, error }`.
|
|
280
|
+
*
|
|
281
|
+
* `url` mirrors `useUser().photoUrl`. `upload(file)` takes a browser `File`
|
|
282
|
+
* on the web Player and expo-file-system's `File` on the Expo export: the
|
|
283
|
+
* file `<FilePicker>` gives you, or `asset.file` from `useCamera()` /
|
|
284
|
+
* `useImageEditor()` — never the asset itself. The server crops the image to
|
|
285
|
+
* a square and resizes it; PNG, JPEG or WebP, 10 MB at most. A refusal
|
|
286
|
+
* carries `err.status`: 413 too large, 415 unsupported type, 400 unreadable
|
|
287
|
+
* image. `uploading` is true while any `upload` / `remove` is in flight.
|
|
288
|
+
*
|
|
289
|
+
* The host owns the request and its own session (`ctx.account`), so a change
|
|
290
|
+
* reaches every widget's `useUser().photoUrl` at once. `ctx.account` is
|
|
291
|
+
* optional: where it is absent (the Builder canvas preview) or nobody is
|
|
292
|
+
* signed in, `canEdit` is false and `upload` / `remove` reject instead of the
|
|
293
|
+
* hook throwing at render.
|
|
294
|
+
*
|
|
295
|
+
* There is no way to read ANOTHER user's photo: `useUsers()` and
|
|
296
|
+
* `useDirectory()` rows carry none.
|
|
297
|
+
*/
|
|
298
|
+
export function useUserPhoto() {
|
|
299
|
+
const ctx = useWidgetContextOrThrow("useUserPhoto");
|
|
300
|
+
const user = ctx.user;
|
|
301
|
+
const account = ctx.account;
|
|
302
|
+
const hostCanEdit =
|
|
303
|
+
Boolean(account) &&
|
|
304
|
+
typeof account.setPhoto === "function" &&
|
|
305
|
+
typeof account.removePhoto === "function";
|
|
306
|
+
const signedIn = Boolean(user && user.id);
|
|
307
|
+
const canEdit = hostCanEdit && signedIn;
|
|
308
|
+
const url =
|
|
309
|
+
user && typeof user.photoUrl === "string" && user.photoUrl
|
|
310
|
+
? user.photoUrl
|
|
311
|
+
: null;
|
|
312
|
+
|
|
313
|
+
const accountRef = useRef(null);
|
|
314
|
+
accountRef.current = hostCanEdit ? account : null;
|
|
315
|
+
const refusalRef = useRef(null);
|
|
316
|
+
refusalRef.current = !hostCanEdit
|
|
317
|
+
? USER_PHOTO_UNAVAILABLE
|
|
318
|
+
: !signedIn
|
|
319
|
+
? USER_PHOTO_SIGNED_OUT
|
|
320
|
+
: null;
|
|
321
|
+
|
|
322
|
+
const inFlightRef = useRef(0);
|
|
323
|
+
const [uploading, setUploading] = useState(false);
|
|
324
|
+
const [error, setError] = useState(null);
|
|
325
|
+
|
|
326
|
+
const change = useCallback(async (perform, invalidCall) => {
|
|
327
|
+
const refusal = refusalRef.current || invalidCall;
|
|
328
|
+
if (refusal) {
|
|
329
|
+
const err = new Error(refusal);
|
|
330
|
+
setError(err);
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
inFlightRef.current += 1;
|
|
334
|
+
setUploading(true);
|
|
335
|
+
setError(null);
|
|
336
|
+
try {
|
|
337
|
+
return await perform(accountRef.current);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
setError(err);
|
|
340
|
+
throw err;
|
|
341
|
+
} finally {
|
|
342
|
+
// upload() and remove() may overlap; the last one clears it.
|
|
343
|
+
inFlightRef.current -= 1;
|
|
344
|
+
setUploading(inFlightRef.current > 0);
|
|
345
|
+
}
|
|
346
|
+
}, []);
|
|
347
|
+
|
|
348
|
+
const upload = useCallback(
|
|
349
|
+
(file) =>
|
|
350
|
+
change(
|
|
351
|
+
(host) => host.setPhoto(file),
|
|
352
|
+
file ? null : USER_PHOTO_NO_FILE,
|
|
353
|
+
),
|
|
354
|
+
[change],
|
|
355
|
+
);
|
|
356
|
+
const remove = useCallback(
|
|
357
|
+
() => change((host) => host.removePhoto()),
|
|
358
|
+
[change],
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
return { url, canEdit, upload, remove, uploading, error };
|
|
260
362
|
}
|
|
261
363
|
|
|
262
364
|
/**
|
|
@@ -3030,11 +3132,11 @@ export function useAgent(agentRef) {
|
|
|
3030
3132
|
cancelled ||
|
|
3031
3133
|
agentIdRef.current !== agentId ||
|
|
3032
3134
|
resumeGenerationRef.current !== generation ||
|
|
3033
|
-
suppressResumeRef.current ||
|
|
3034
|
-
// A `send` that started during the resume already latched a thread.
|
|
3035
|
-
// Overwriting it would orphan that PAID turn in a conversation
|
|
3036
|
-
// nothing reads again, and point the next send somewhere else.
|
|
3037
|
-
conversationRef.current
|
|
3135
|
+
suppressResumeRef.current ||
|
|
3136
|
+
// A `send` that started during the resume already latched a thread.
|
|
3137
|
+
// Overwriting it would orphan that PAID turn in a conversation
|
|
3138
|
+
// nothing reads again, and point the next send somewhere else.
|
|
3139
|
+
conversationRef.current
|
|
3038
3140
|
) {
|
|
3039
3141
|
return;
|
|
3040
3142
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -646,9 +646,21 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
646
646
|
id: string | null;
|
|
647
647
|
email: string | null;
|
|
648
648
|
displayName: string | null;
|
|
649
|
+
/** The user's own profile photo as an absolute URL; null when none is set. */
|
|
650
|
+
photoUrl: string | null;
|
|
649
651
|
roles: string[];
|
|
650
652
|
groupIds: string[];
|
|
651
653
|
};
|
|
654
|
+
/**
|
|
655
|
+
* Optional self-service account actions backing `useUserPhoto()`. The host
|
|
656
|
+
* performs the request and updates its own session, so `user.photoUrl`
|
|
657
|
+
* changes for every widget. Absent where the photo cannot be changed (the
|
|
658
|
+
* Builder canvas preview); `useUserPhoto().canEdit` is then false.
|
|
659
|
+
*/
|
|
660
|
+
account?: {
|
|
661
|
+
setPhoto(file: UserPhotoFile): Promise<{ photoUrl: string | null }>;
|
|
662
|
+
removePhoto(): Promise<{ photoUrl: null }>;
|
|
663
|
+
};
|
|
652
664
|
workspace: {
|
|
653
665
|
id: string;
|
|
654
666
|
slug: string;
|
|
@@ -1510,10 +1522,54 @@ export function useUser(): {
|
|
|
1510
1522
|
id: string | null;
|
|
1511
1523
|
email: string | null;
|
|
1512
1524
|
displayName: string | null;
|
|
1525
|
+
/**
|
|
1526
|
+
* The user's own profile photo as an absolute URL, ready for `<Avatar url>`
|
|
1527
|
+
* or `<Image source={{ uri }}>`. Null when none is set, for anonymous
|
|
1528
|
+
* visitors, and on a host that does not provide it.
|
|
1529
|
+
*/
|
|
1530
|
+
photoUrl: string | null;
|
|
1513
1531
|
roles: string[];
|
|
1514
1532
|
groupIds: string[];
|
|
1515
1533
|
};
|
|
1516
1534
|
|
|
1535
|
+
/**
|
|
1536
|
+
* The file `useUserPhoto().upload` takes: a browser `File` on the web Player
|
|
1537
|
+
* and expo-file-system's `File` on the Expo export. It is the file
|
|
1538
|
+
* `<FilePicker>` gives you, or `asset.file` from `useCamera()` /
|
|
1539
|
+
* `useImageEditor()` — never the asset itself.
|
|
1540
|
+
*/
|
|
1541
|
+
export type UserPhotoFile = Blob & { name?: string };
|
|
1542
|
+
|
|
1543
|
+
export interface UserPhotoApi {
|
|
1544
|
+
/** Same value as `useUser().photoUrl`. */
|
|
1545
|
+
url: string | null;
|
|
1546
|
+
/** True when a user is signed in and the host can change the photo. */
|
|
1547
|
+
canEdit: boolean;
|
|
1548
|
+
/**
|
|
1549
|
+
* Replace the photo. The server crops the image to a square and resizes it.
|
|
1550
|
+
* PNG, JPEG or WebP, 10 MB at most. A refusal carries `status`: 413 too
|
|
1551
|
+
* large, 415 unsupported type, 400 unreadable image. Rejects, and sets
|
|
1552
|
+
* `error`, when `canEdit` is false or no file is passed.
|
|
1553
|
+
*/
|
|
1554
|
+
upload(file: UserPhotoFile): Promise<{ photoUrl: string | null }>;
|
|
1555
|
+
/** Clear the photo. Rejects when `canEdit` is false. */
|
|
1556
|
+
remove(): Promise<{ photoUrl: null }>;
|
|
1557
|
+
/** True while any `upload` or `remove` call is in flight. */
|
|
1558
|
+
uploading: boolean;
|
|
1559
|
+
/** The last failure; cleared by the next attempt. */
|
|
1560
|
+
error: (Error & { status?: number }) | null;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* Read and change the signed-in user's OWN profile photo. Never throws at
|
|
1565
|
+
* render: where nobody is signed in, or the host cannot change the photo,
|
|
1566
|
+
* `canEdit` is false and `upload` / `remove` reject. Pass `upload` the
|
|
1567
|
+
* file `<FilePicker>` gives you, or `asset.file` from `useCamera()` /
|
|
1568
|
+
* `useImageEditor()`. There is no way to read another user's photo;
|
|
1569
|
+
* `useUsers()` and `useDirectory()` rows carry none.
|
|
1570
|
+
*/
|
|
1571
|
+
export function useUserPhoto(): UserPhotoApi;
|
|
1572
|
+
|
|
1517
1573
|
/**
|
|
1518
1574
|
* REQ-LAY-08 — returns `true` when the host has sized this widget to fill its
|
|
1519
1575
|
* layout slot's available height (a page-grid tile set to "Fill tile height",
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/dist/linter.cjs
CHANGED
|
@@ -252,7 +252,9 @@ function _packageRoot(spec) {
|
|
|
252
252
|
const parts = spec.split("/");
|
|
253
253
|
// A traversal / empty segment is a non-canonical specifier — never resolve it
|
|
254
254
|
// to a vetted root, or "leaflet/../evil" would pass the gate as "leaflet".
|
|
255
|
-
if (parts.some((p) => p === "" || p === "." || p === ".."))
|
|
255
|
+
if (parts.some((p) => p === "" || p === "." || p === ".." || p.includes("\\"))) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
256
258
|
return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
257
259
|
}
|
|
258
260
|
|
|
@@ -336,6 +338,87 @@ function _importRules(source, manifest) {
|
|
|
336
338
|
return findings;
|
|
337
339
|
}
|
|
338
340
|
|
|
341
|
+
// sc-10466 — Leaflet is bundled per widget; no host supplies its stylesheet.
|
|
342
|
+
// Without `leaflet/dist/leaflet.css` the tiles, panes and markers are
|
|
343
|
+
// unpositioned: a patch of map in an empty box, and no visible pin. And once
|
|
344
|
+
// the CSS is injected inline its default marker image resolves against the
|
|
345
|
+
// page URL and 404s, so a `<Marker>` needs an explicit `icon`.
|
|
346
|
+
const _LEAFLET_ROOTS = new Set(["leaflet", "react-leaflet"]);
|
|
347
|
+
const _LEAFLET_CSS = "leaflet/dist/leaflet.css";
|
|
348
|
+
const _MARKER_TAG_RE = /<Marker\b/g;
|
|
349
|
+
|
|
350
|
+
const _ASSET_SPECIFIER_RE = /\.(css|png|jpe?g|gif)$/i;
|
|
351
|
+
const _ICON_DEFAULT_CONFIGURED_RE =
|
|
352
|
+
/\bIcon\s*\.\s*Default\s*(\.\s*mergeOptions\s*\(|\.\s*imagePath\s*=)/;
|
|
353
|
+
|
|
354
|
+
function _lineAt(code, index) {
|
|
355
|
+
return code.slice(0, index).split(/\r?\n/).length;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function _leafletRules(source) {
|
|
359
|
+
const code = _stripNonCode(source, { keepStrings: true });
|
|
360
|
+
const sourceLines = source.split(/\r?\n/);
|
|
361
|
+
let firstLeafletLine = 0;
|
|
362
|
+
let importsCss = false;
|
|
363
|
+
let importsReactLeaflet = false;
|
|
364
|
+
// Whole-source scan: a Prettier-wrapped import spans several lines.
|
|
365
|
+
IMPORT_SPECIFIER_RE.lastIndex = 0;
|
|
366
|
+
let m;
|
|
367
|
+
while ((m = IMPORT_SPECIFIER_RE.exec(code))) {
|
|
368
|
+
const spec = m[1];
|
|
369
|
+
if (spec === _LEAFLET_CSS) importsCss = true;
|
|
370
|
+
const root = _packageRoot(spec);
|
|
371
|
+
if (!_LEAFLET_ROOTS.has(root) || _ASSET_SPECIFIER_RE.test(spec)) continue;
|
|
372
|
+
if (root === "react-leaflet") importsReactLeaflet = true;
|
|
373
|
+
if (!firstLeafletLine) firstLeafletLine = _lineAt(code, m.index + m[0].length);
|
|
374
|
+
}
|
|
375
|
+
IMPORT_SPECIFIER_RE.lastIndex = 0;
|
|
376
|
+
if (!firstLeafletLine) return [];
|
|
377
|
+
const findings = [];
|
|
378
|
+
if (!importsCss) {
|
|
379
|
+
findings.push({
|
|
380
|
+
rule: "leaflet-css-missing",
|
|
381
|
+
severity: "error",
|
|
382
|
+
label:
|
|
383
|
+
`this file uses Leaflet but never imports its stylesheet — without ` +
|
|
384
|
+
`it the map draws a partial patch of tiles inside an empty box and ` +
|
|
385
|
+
`markers are invisible. Add \`import "${_LEAFLET_CSS}";\` to this file.`,
|
|
386
|
+
line: firstLeafletLine,
|
|
387
|
+
snippet: (sourceLines[firstLeafletLine - 1] || "").trim().slice(0, 200),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
if (
|
|
391
|
+
!importsReactLeaflet ||
|
|
392
|
+
_ICON_DEFAULT_CONFIGURED_RE.test(_stripNonCode(source))
|
|
393
|
+
) {
|
|
394
|
+
return findings;
|
|
395
|
+
}
|
|
396
|
+
_MARKER_TAG_RE.lastIndex = 0;
|
|
397
|
+
let tag;
|
|
398
|
+
while ((tag = _MARKER_TAG_RE.exec(code))) {
|
|
399
|
+
const end = _jsxOpenTagEnd(code, tag.index + tag[0].length);
|
|
400
|
+
if (/(^|\s)icon\s*=/.test(code.slice(tag.index, end))) continue;
|
|
401
|
+
const line = _lineAt(code, tag.index);
|
|
402
|
+
findings.push({
|
|
403
|
+
rule: "leaflet-default-marker-icon",
|
|
404
|
+
severity: "warning",
|
|
405
|
+
label:
|
|
406
|
+
`<Marker> uses Leaflet's default icon, whose image path cannot ` +
|
|
407
|
+
`resolve inside a widget bundle, so the pin does not show. Import the ` +
|
|
408
|
+
`images (\`import markerIcon from "leaflet/dist/images/marker-icon.png"\`, ` +
|
|
409
|
+
`plus marker-icon-2x.png and marker-shadow.png), build ` +
|
|
410
|
+
`\`L.icon({ iconUrl, iconRetinaUrl, shadowUrl, iconSize: [25, 41], ` +
|
|
411
|
+
`iconAnchor: [12, 41] })\` once at module scope, and pass it as ` +
|
|
412
|
+
`\`icon={…}\`. Warning only — a widget that sets \`L.Icon.Default\` ` +
|
|
413
|
+
`itself (\`mergeOptions\` / \`imagePath\`) is not flagged.`,
|
|
414
|
+
line,
|
|
415
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
416
|
+
});
|
|
417
|
+
_MARKER_TAG_RE.lastIndex = end;
|
|
418
|
+
}
|
|
419
|
+
return findings;
|
|
420
|
+
}
|
|
421
|
+
|
|
339
422
|
// Explicit opt-out. A widget legitimately calling a THIRD-PARTY API with a
|
|
340
423
|
// relative path against an `axios` baseURL cannot avoid the substring match,
|
|
341
424
|
// and post-sc-3589 an unavoidable finding burns AI repair turns on correct
|
|
@@ -1853,6 +1936,7 @@ function lintSource(source, options) {
|
|
|
1853
1936
|
severity: f.severity || "error",
|
|
1854
1937
|
})),
|
|
1855
1938
|
);
|
|
1939
|
+
findings.push(..._leafletRules(source));
|
|
1856
1940
|
findings.push(..._hostApiUrlRules(source));
|
|
1857
1941
|
// sc-6970 — soft warning: HTML markup spliced into a content string.
|
|
1858
1942
|
findings.push(..._htmlInContentRules(source));
|
package/dist/linter.js
CHANGED
|
@@ -135,7 +135,9 @@ function _packageRoot(spec) {
|
|
|
135
135
|
const parts = spec.split("/");
|
|
136
136
|
// A traversal / empty segment is a non-canonical specifier — never resolve it
|
|
137
137
|
// to a vetted root, or "leaflet/../evil" would pass the gate as "leaflet".
|
|
138
|
-
if (parts.some((p) => p === "" || p === "." || p === ".."))
|
|
138
|
+
if (parts.some((p) => p === "" || p === "." || p === ".." || p.includes("\\"))) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
139
141
|
return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
140
142
|
}
|
|
141
143
|
|
|
@@ -229,6 +231,87 @@ function _importRules(source, manifest) {
|
|
|
229
231
|
return findings;
|
|
230
232
|
}
|
|
231
233
|
|
|
234
|
+
// sc-10466 — Leaflet is bundled per widget; no host supplies its stylesheet.
|
|
235
|
+
// Without `leaflet/dist/leaflet.css` the tiles, panes and markers are
|
|
236
|
+
// unpositioned: a patch of map in an empty box, and no visible pin. And once
|
|
237
|
+
// the CSS is injected inline its default marker image resolves against the
|
|
238
|
+
// page URL and 404s, so a `<Marker>` needs an explicit `icon`.
|
|
239
|
+
const _LEAFLET_ROOTS = new Set(["leaflet", "react-leaflet"]);
|
|
240
|
+
const _LEAFLET_CSS = "leaflet/dist/leaflet.css";
|
|
241
|
+
const _MARKER_TAG_RE = /<Marker\b/g;
|
|
242
|
+
|
|
243
|
+
const _ASSET_SPECIFIER_RE = /\.(css|png|jpe?g|gif)$/i;
|
|
244
|
+
const _ICON_DEFAULT_CONFIGURED_RE =
|
|
245
|
+
/\bIcon\s*\.\s*Default\s*(\.\s*mergeOptions\s*\(|\.\s*imagePath\s*=)/;
|
|
246
|
+
|
|
247
|
+
function _lineAt(code, index) {
|
|
248
|
+
return code.slice(0, index).split(/\r?\n/).length;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function _leafletRules(source) {
|
|
252
|
+
const code = _stripNonCode(source, { keepStrings: true });
|
|
253
|
+
const sourceLines = source.split(/\r?\n/);
|
|
254
|
+
let firstLeafletLine = 0;
|
|
255
|
+
let importsCss = false;
|
|
256
|
+
let importsReactLeaflet = false;
|
|
257
|
+
// Whole-source scan: a Prettier-wrapped import spans several lines.
|
|
258
|
+
IMPORT_SPECIFIER_RE.lastIndex = 0;
|
|
259
|
+
let m;
|
|
260
|
+
while ((m = IMPORT_SPECIFIER_RE.exec(code))) {
|
|
261
|
+
const spec = m[1];
|
|
262
|
+
if (spec === _LEAFLET_CSS) importsCss = true;
|
|
263
|
+
const root = _packageRoot(spec);
|
|
264
|
+
if (!_LEAFLET_ROOTS.has(root) || _ASSET_SPECIFIER_RE.test(spec)) continue;
|
|
265
|
+
if (root === "react-leaflet") importsReactLeaflet = true;
|
|
266
|
+
if (!firstLeafletLine) firstLeafletLine = _lineAt(code, m.index + m[0].length);
|
|
267
|
+
}
|
|
268
|
+
IMPORT_SPECIFIER_RE.lastIndex = 0;
|
|
269
|
+
if (!firstLeafletLine) return [];
|
|
270
|
+
const findings = [];
|
|
271
|
+
if (!importsCss) {
|
|
272
|
+
findings.push({
|
|
273
|
+
rule: "leaflet-css-missing",
|
|
274
|
+
severity: "error",
|
|
275
|
+
label:
|
|
276
|
+
`this file uses Leaflet but never imports its stylesheet — without ` +
|
|
277
|
+
`it the map draws a partial patch of tiles inside an empty box and ` +
|
|
278
|
+
`markers are invisible. Add \`import "${_LEAFLET_CSS}";\` to this file.`,
|
|
279
|
+
line: firstLeafletLine,
|
|
280
|
+
snippet: (sourceLines[firstLeafletLine - 1] || "").trim().slice(0, 200),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (
|
|
284
|
+
!importsReactLeaflet ||
|
|
285
|
+
_ICON_DEFAULT_CONFIGURED_RE.test(_stripNonCode(source))
|
|
286
|
+
) {
|
|
287
|
+
return findings;
|
|
288
|
+
}
|
|
289
|
+
_MARKER_TAG_RE.lastIndex = 0;
|
|
290
|
+
let tag;
|
|
291
|
+
while ((tag = _MARKER_TAG_RE.exec(code))) {
|
|
292
|
+
const end = _jsxOpenTagEnd(code, tag.index + tag[0].length);
|
|
293
|
+
if (/(^|\s)icon\s*=/.test(code.slice(tag.index, end))) continue;
|
|
294
|
+
const line = _lineAt(code, tag.index);
|
|
295
|
+
findings.push({
|
|
296
|
+
rule: "leaflet-default-marker-icon",
|
|
297
|
+
severity: "warning",
|
|
298
|
+
label:
|
|
299
|
+
`<Marker> uses Leaflet's default icon, whose image path cannot ` +
|
|
300
|
+
`resolve inside a widget bundle, so the pin does not show. Import the ` +
|
|
301
|
+
`images (\`import markerIcon from "leaflet/dist/images/marker-icon.png"\`, ` +
|
|
302
|
+
`plus marker-icon-2x.png and marker-shadow.png), build ` +
|
|
303
|
+
`\`L.icon({ iconUrl, iconRetinaUrl, shadowUrl, iconSize: [25, 41], ` +
|
|
304
|
+
`iconAnchor: [12, 41] })\` once at module scope, and pass it as ` +
|
|
305
|
+
`\`icon={…}\`. Warning only — a widget that sets \`L.Icon.Default\` ` +
|
|
306
|
+
`itself (\`mergeOptions\` / \`imagePath\`) is not flagged.`,
|
|
307
|
+
line,
|
|
308
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
309
|
+
});
|
|
310
|
+
_MARKER_TAG_RE.lastIndex = end;
|
|
311
|
+
}
|
|
312
|
+
return findings;
|
|
313
|
+
}
|
|
314
|
+
|
|
232
315
|
// REQ-WSDK-PLATFORM §3.5: soft warning when source contains host-API URL
|
|
233
316
|
// substrings. `severity: "warning"` so a HUMAN-authored submission still
|
|
234
317
|
// publishes and the marketplace review queue flags it for a human pass.
|
|
@@ -1925,6 +2008,7 @@ export function lintSource(source, options) {
|
|
|
1925
2008
|
severity: f.severity || "error",
|
|
1926
2009
|
})),
|
|
1927
2010
|
);
|
|
2011
|
+
findings.push(..._leafletRules(source));
|
|
1928
2012
|
// REQ-WSDK-PLATFORM §3.5: soft host-API URL warning (does not block).
|
|
1929
2013
|
findings.push(..._hostApiUrlRules(source));
|
|
1930
2014
|
// sc-6970 — soft warning: HTML markup spliced into a content string.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.188.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|