@raster-app/react 0.0.0-stage → 0.1.0-canary-fd0f8dd

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -2
  3. package/dist/context.d.ts +30 -0
  4. package/dist/context.js +50 -0
  5. package/dist/context.js.map +1 -0
  6. package/dist/icons.d.ts +22 -0
  7. package/dist/icons.js +21 -0
  8. package/dist/icons.js.map +1 -0
  9. package/dist/index.d.ts +13 -0
  10. package/dist/index.js +15 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/picker-parts.d.ts +71 -0
  13. package/dist/picker-parts.js +150 -0
  14. package/dist/picker-parts.js.map +1 -0
  15. package/dist/picker-sign-in.d.ts +7 -0
  16. package/dist/picker-sign-in.js +18 -0
  17. package/dist/picker-sign-in.js.map +1 -0
  18. package/dist/query-keys.d.ts +13 -0
  19. package/dist/query-keys.js +14 -0
  20. package/dist/query-keys.js.map +1 -0
  21. package/dist/raster-picker.d.ts +23 -0
  22. package/dist/raster-picker.js +188 -0
  23. package/dist/raster-picker.js.map +1 -0
  24. package/dist/state-store.d.ts +18 -0
  25. package/dist/state-store.js +47 -0
  26. package/dist/state-store.js.map +1 -0
  27. package/dist/styles.css +1000 -0
  28. package/dist/use-asset-search.d.ts +28 -0
  29. package/dist/use-asset-search.js +82 -0
  30. package/dist/use-asset-search.js.map +1 -0
  31. package/dist/use-asset-upload.d.ts +46 -0
  32. package/dist/use-asset-upload.js +73 -0
  33. package/dist/use-asset-upload.js.map +1 -0
  34. package/dist/use-assets.d.ts +24 -0
  35. package/dist/use-assets.js +63 -0
  36. package/dist/use-assets.js.map +1 -0
  37. package/dist/use-libraries.d.ts +19 -0
  38. package/dist/use-libraries.js +57 -0
  39. package/dist/use-libraries.js.map +1 -0
  40. package/dist/use-navigation.d.ts +31 -0
  41. package/dist/use-navigation.js +111 -0
  42. package/dist/use-navigation.js.map +1 -0
  43. package/dist/use-organizations.d.ts +36 -0
  44. package/dist/use-organizations.js +23 -0
  45. package/dist/use-organizations.js.map +1 -0
  46. package/dist/use-promote-variant.d.ts +43 -0
  47. package/dist/use-promote-variant.js +65 -0
  48. package/dist/use-promote-variant.js.map +1 -0
  49. package/dist/use-session.d.ts +22 -0
  50. package/dist/use-session.js +37 -0
  51. package/dist/use-session.js.map +1 -0
  52. package/dist/use-sign-in.d.ts +34 -0
  53. package/dist/use-sign-in.js +79 -0
  54. package/dist/use-sign-in.js.map +1 -0
  55. package/package.json +54 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Monogram
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,123 @@
1
- # Temporary Holding Version
1
+ # @raster-app/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Raster's asset picker for React, and hooks to build your own Raster UI.
4
+
5
+ ## Documentation
6
+
7
+ [raster.app/docs](https://raster.app/docs)
8
+
9
+ ## Requirements
10
+
11
+ - React 18 or 19
12
+ - Node.js 24 or later
13
+
14
+ ## Installation
15
+
16
+ ```sh
17
+ npm install @raster-app/react
18
+ ```
19
+
20
+ It includes [`@raster-app/sdk`](https://www.npmjs.com/package/@raster-app/sdk) and re-exports all
21
+ of it, so import the client, `toUserMessage` and types such as `Asset` from `@raster-app/react`.
22
+
23
+ ## Usage
24
+
25
+ ### Add the picker
26
+
27
+ ```tsx
28
+ import {
29
+ RasterPicker,
30
+ RasterProvider,
31
+ getFullSizeUrl,
32
+ localStorageCredentialStore,
33
+ } from '@raster-app/react'
34
+ import '@raster-app/react/styles.css'
35
+
36
+ export function Picker({ onImage }: { onImage: (url: string) => void }) {
37
+ return (
38
+ <RasterProvider
39
+ config={{
40
+ host: { name: 'My App', openExternal: (url) => window.open(url, '_blank') },
41
+ credentials: localStorageCredentialStore(),
42
+ }}
43
+ >
44
+ <RasterPicker onPick={(asset) => onImage(getFullSizeUrl(asset) ?? '')} />
45
+ </RasterProvider>
46
+ )
47
+ }
48
+ ```
49
+
50
+ `host.name` is the name Raster's sign-in page shows for your app, and `openExternal` opens that page
51
+ in the user's browser. `credentials` is where the sign-in is kept.
52
+
53
+ The picker signs the user in, browses their organizations and libraries, searches, uploads, and
54
+ lets them pick an asset or one of its versions. It fills its container.
55
+
56
+ | Prop | What it does |
57
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
58
+ | `onPick(asset, organizationId)` | Called with the picked asset. `getFullSizeUrl(asset)` is its image. |
59
+ | `organizationId` | Limits sign-in to this organization. |
60
+ | `scope` | `'org'` to ask for one organization, `'user'` for the whole account. Defaults to `'org'` with an `organizationId`. |
61
+ | `onError(error)` | Called with each error the picker shows, for logging. |
62
+ | `theme` | `'light'` or `'dark'`. Follows the system when unset. |
63
+
64
+ ### Build your own UI
65
+
66
+ The same provider gives your components hooks.
67
+
68
+ ```tsx
69
+ import {
70
+ RasterProvider,
71
+ createRasterClient,
72
+ localStorageCredentialStore,
73
+ useAssets,
74
+ useSession,
75
+ } from '@raster-app/react'
76
+
77
+ // Create the client once, outside any component.
78
+ const client = createRasterClient({
79
+ host: { name: 'My App', openExternal: (url) => window.open(url, '_blank') },
80
+ credentials: localStorageCredentialStore(),
81
+ })
82
+
83
+ export function App() {
84
+ return (
85
+ <RasterProvider client={client}>
86
+ <Library />
87
+ </RasterProvider>
88
+ )
89
+ }
90
+
91
+ function Library() {
92
+ const { credentials, isConfigured } = useSession()
93
+ const { assets, hasMore, loadMore } = useAssets({ organizationId: 'acme', libraryId: 'lib-1' })
94
+
95
+ if (credentials === undefined) return null // still loading
96
+ if (!isConfigured) return <SignIn />
97
+ return <Grid assets={assets ?? []} onEnd={hasMore ? loadMore : undefined} />
98
+ }
99
+ ```
100
+
101
+ | Hook | What it gives you |
102
+ | ------------------------------------------ | ---------------------------------------------------------------------- |
103
+ | `useSession()` | `isConfigured`, `isSignedIn`, `userName` and `signOut()`. |
104
+ | `useSignIn()` | Sign-in with Raster's device flow or an API key. |
105
+ | `useOrganizations()` | The organizations the user can reach, in `data`. |
106
+ | `useLibraries({ organizationId })` | An organization's libraries, in `data`, each with the user's `access`. |
107
+ | `useAssets({ organizationId, libraryId })` | A library's `assets`, with `loadMore()`. |
108
+ | `useAssetSearch(scope, query)` | Search results, `hits` and `found`, as the user types. |
109
+ | `useAssetUpload()` | `upload()` a file, then wait for Raster to process it. |
110
+ | `usePromoteVariant()` | Make a variant its asset's default. |
111
+ | `useRasterNavigation({ organizations })` | The open organization, library and asset, remembered between visits. |
112
+
113
+ `RasterProvider` takes a `client`, or a `config` to create one, and `state`, where navigation is
114
+ remembered: `memoryStateStore()` by default, or `localStorageStateStore()`.
115
+
116
+ ### Errors
117
+
118
+ Every failure is a `RasterApiError`. Show the user `toUserMessage(error)`, and branch on
119
+ `error.code`.
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,30 @@
1
+ import { QueryClient } from '@tanstack/react-query';
2
+ import { type ReactNode } from 'react';
3
+ import { type RasterClient, type RasterClientConfig } from '@raster-app/sdk';
4
+ import { type StateStore } from './state-store.js';
5
+ export type RasterProviderProps = {
6
+ children: ReactNode;
7
+ /** Where `useRasterNavigation` remembers the last organization and library. Defaults to memory. */
8
+ state?: StateStore;
9
+ /**
10
+ * The host's own `QueryClient`, if it has one, from the same TanStack Query major as this
11
+ * package. Without it the provider creates one, which shadows the host's for everything below.
12
+ */
13
+ queryClient?: QueryClient;
14
+ } & ({
15
+ /** An existing client. Use this when the host builds one itself. */
16
+ client: RasterClient;
17
+ config?: never;
18
+ } | {
19
+ /** Config for a client the provider creates and owns. */
20
+ config: RasterClientConfig;
21
+ client?: never;
22
+ });
23
+ /**
24
+ * Provides one `RasterClient` to the tree, and the `QueryClient` its hooks cache in. Both are
25
+ * fixed on mount: a second client means a second session store, and two stores refreshing one
26
+ * credential spend each other's refresh token. Remount with a new `key` to switch.
27
+ */
28
+ export declare function RasterProvider({ children, client, config, state, queryClient, }: RasterProviderProps): import("react").JSX.Element;
29
+ export declare function useRasterClient(): RasterClient;
30
+ export declare function useStateStore(): StateStore;
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
+ import { createContext, useContext, useEffect, useState } from 'react';
4
+ import { createRasterClient } from '@raster-app/sdk';
5
+ import { rasterKeys } from './query-keys.js';
6
+ import { memoryStateStore } from './state-store.js';
7
+ const RasterContext = createContext(null);
8
+ /**
9
+ * Provides one `RasterClient` to the tree, and the `QueryClient` its hooks cache in. Both are
10
+ * fixed on mount: a second client means a second session store, and two stores refreshing one
11
+ * credential spend each other's refresh token. Remount with a new `key` to switch.
12
+ */
13
+ export function RasterProvider({ children, client, config, state, queryClient, }) {
14
+ const [value] = useState(() => ({
15
+ client: client ?? createRasterClient(config),
16
+ state: state ?? memoryStateStore(),
17
+ queryClient: queryClient ??
18
+ // The Raster app's own defaults. Focus returns to a plugin frame on every click into it,
19
+ // and a refetch each time would replay every page of a long grid.
20
+ new QueryClient({
21
+ defaultOptions: { queries: { staleTime: 60 * 1000, refetchOnWindowFocus: false } },
22
+ }),
23
+ }));
24
+ // A sign-in's cache goes when it ends: a sign-out, or somebody else signing in.
25
+ useEffect(() => {
26
+ let identity = value.client.session.credentialIdentity();
27
+ return value.client.session.subscribe(() => {
28
+ const next = value.client.session.credentialIdentity();
29
+ if (identity !== null && next !== identity) {
30
+ value.queryClient.removeQueries({ queryKey: rasterKeys.signIn(identity) });
31
+ }
32
+ identity = next;
33
+ });
34
+ }, [value]);
35
+ return (_jsx(QueryClientProvider, { client: value.queryClient, children: _jsx(RasterContext.Provider, { value: value, children: children }) }));
36
+ }
37
+ function useRaster() {
38
+ const value = useContext(RasterContext);
39
+ if (value === null) {
40
+ throw new Error('Raster hooks must be used inside a <RasterProvider>.');
41
+ }
42
+ return value;
43
+ }
44
+ export function useRasterClient() {
45
+ return useRaster().client;
46
+ }
47
+ export function useStateStore() {
48
+ return useRaster().state;
49
+ }
50
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AACxE,OAAO,EAAkB,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEtF,OAAO,EAA8C,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEhG,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAmB,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAEpE,MAAM,aAAa,GAAG,aAAa,CAAqD,IAAI,CAAC,CAAA;AAwB7F;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,EAC9B,QAAQ,EACR,MAAM,EACN,MAAM,EACN,KAAK,EACL,WAAW,GACU;IACrB,MAAM,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC;QAC/B,MAAM,EAAE,MAAM,IAAI,kBAAkB,CAAC,MAAM,CAAC;QAC5C,KAAK,EAAE,KAAK,IAAI,gBAAgB,EAAE;QAClC,WAAW,EACV,WAAW;YACX,yFAAyF;YACzF,kEAAkE;YAClE,IAAI,WAAW,CAAC;gBACf,cAAc,EAAE,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,EAAE,oBAAoB,EAAE,KAAK,EAAE,EAAE;aAClF,CAAC;KACH,CAAC,CAAC,CAAA;IAEH,gFAAgF;IAChF,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAA;QACxD,OAAO,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE;YAC1C,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAA;YACtD,IAAI,QAAQ,KAAK,IAAI,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC5C,KAAK,CAAC,WAAW,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;YAC3E,CAAC;YACD,QAAQ,GAAG,IAAI,CAAA;QAChB,CAAC,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAEX,OAAO,CACN,KAAC,mBAAmB,IAAC,MAAM,EAAE,KAAK,CAAC,WAAW,YAC7C,KAAC,aAAa,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA0B,GACpD,CACtB,CAAA;AACF,CAAC;AAED,SAAS,SAAS;IACjB,MAAM,KAAK,GAAG,UAAU,CAAC,aAAa,CAAC,CAAA;IACvC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAA;IACxE,CAAC;IACD,OAAO,KAAK,CAAA;AACb,CAAC;AAED,MAAM,UAAU,eAAe;IAC9B,OAAO,SAAS,EAAE,CAAC,MAAM,CAAA;AAC1B,CAAC;AAED,MAAM,UAAU,aAAa;IAC5B,OAAO,SAAS,EAAE,CAAC,KAAK,CAAA;AACzB,CAAC","sourcesContent":["import { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { type ReactNode, createContext, useContext, useEffect, useState } from 'react'\n\nimport { type RasterClient, type RasterClientConfig, createRasterClient } from '@raster-app/sdk'\n\nimport { rasterKeys } from './query-keys.js'\nimport { type StateStore, memoryStateStore } from './state-store.js'\n\nconst RasterContext = createContext<{ client: RasterClient; state: StateStore } | null>(null)\n\nexport type RasterProviderProps = {\n\tchildren: ReactNode\n\t/** Where `useRasterNavigation` remembers the last organization and library. Defaults to memory. */\n\tstate?: StateStore\n\t/**\n\t * The host's own `QueryClient`, if it has one, from the same TanStack Query major as this\n\t * package. Without it the provider creates one, which shadows the host's for everything below.\n\t */\n\tqueryClient?: QueryClient\n} & (\n\t| {\n\t\t\t/** An existing client. Use this when the host builds one itself. */\n\t\t\tclient: RasterClient\n\t\t\tconfig?: never\n\t }\n\t| {\n\t\t\t/** Config for a client the provider creates and owns. */\n\t\t\tconfig: RasterClientConfig\n\t\t\tclient?: never\n\t }\n)\n\n/**\n * Provides one `RasterClient` to the tree, and the `QueryClient` its hooks cache in. Both are\n * fixed on mount: a second client means a second session store, and two stores refreshing one\n * credential spend each other's refresh token. Remount with a new `key` to switch.\n */\nexport function RasterProvider({\n\tchildren,\n\tclient,\n\tconfig,\n\tstate,\n\tqueryClient,\n}: RasterProviderProps) {\n\tconst [value] = useState(() => ({\n\t\tclient: client ?? createRasterClient(config),\n\t\tstate: state ?? memoryStateStore(),\n\t\tqueryClient:\n\t\t\tqueryClient ??\n\t\t\t// The Raster app's own defaults. Focus returns to a plugin frame on every click into it,\n\t\t\t// and a refetch each time would replay every page of a long grid.\n\t\t\tnew QueryClient({\n\t\t\t\tdefaultOptions: { queries: { staleTime: 60 * 1000, refetchOnWindowFocus: false } },\n\t\t\t}),\n\t}))\n\n\t// A sign-in's cache goes when it ends: a sign-out, or somebody else signing in.\n\tuseEffect(() => {\n\t\tlet identity = value.client.session.credentialIdentity()\n\t\treturn value.client.session.subscribe(() => {\n\t\t\tconst next = value.client.session.credentialIdentity()\n\t\t\tif (identity !== null && next !== identity) {\n\t\t\t\tvalue.queryClient.removeQueries({ queryKey: rasterKeys.signIn(identity) })\n\t\t\t}\n\t\t\tidentity = next\n\t\t})\n\t}, [value])\n\n\treturn (\n\t\t<QueryClientProvider client={value.queryClient}>\n\t\t\t<RasterContext.Provider value={value}>{children}</RasterContext.Provider>\n\t\t</QueryClientProvider>\n\t)\n}\n\nfunction useRaster() {\n\tconst value = useContext(RasterContext)\n\tif (value === null) {\n\t\tthrow new Error('Raster hooks must be used inside a <RasterProvider>.')\n\t}\n\treturn value\n}\n\nexport function useRasterClient(): RasterClient {\n\treturn useRaster().client\n}\n\nexport function useStateStore(): StateStore {\n\treturn useRaster().state\n}\n"]}
@@ -0,0 +1,22 @@
1
+ /** Raster's glyphs, path for path from raster/apps/web: 24-unit viewBox, 1.5 stroke, round caps. */
2
+ declare const SHAPES: {
3
+ search: import("react").JSX.Element;
4
+ grid: import("react").JSX.Element;
5
+ check: import("react").JSX.Element;
6
+ circle: import("react").JSX.Element;
7
+ layers: import("react").JSX.Element;
8
+ star: import("react").JSX.Element;
9
+ upload: import("react").JSX.Element;
10
+ 'chevron-up-down': import("react").JSX.Element;
11
+ link: import("react").JSX.Element;
12
+ x: import("react").JSX.Element;
13
+ 'arrow-right': import("react").JSX.Element;
14
+ 'info-circle': import("react").JSX.Element;
15
+ raster: import("react").JSX.Element;
16
+ };
17
+ export declare function Icon({ shape, solid, width, }: {
18
+ shape: keyof typeof SHAPES;
19
+ solid?: boolean;
20
+ width?: number;
21
+ }): import("react").JSX.Element;
22
+ export {};
package/dist/icons.js ADDED
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Raster's glyphs, path for path from raster/apps/web: 24-unit viewBox, 1.5 stroke, round caps. */
3
+ const SHAPES = {
4
+ search: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M11 17.25a6.25 6.25 0 110-12.5 6.25 6.25 0 010 12.5z" }), _jsx("path", { d: "M16 16l4.5 4.5" })] })),
5
+ grid: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M3 3h7v7H3z" }), _jsx("path", { d: "M14 3h7v7h-7z" }), _jsx("path", { d: "M14 14h7v7h-7z" }), _jsx("path", { d: "M3 14h7v7H3z" })] })),
6
+ check: _jsx("path", { d: "M20 6L9 17l-5-5" }),
7
+ circle: _jsx("circle", { cx: "12", cy: "12", r: "10" }),
8
+ layers: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M12 2L2 7l10 5 10-5-10-5z" }), _jsx("path", { d: "M2 17l10 5 10-5" }), _jsx("path", { d: "M2 12l10 5 10-5" })] })),
9
+ star: (_jsx("path", { d: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" })),
10
+ upload: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" }), _jsx("path", { d: "M17 8l-5-5-5 5" }), _jsx("path", { d: "M12 3v12" })] })),
11
+ 'chevron-up-down': _jsx("path", { d: "M17 8.517L12 3 7 8.517M7 15.48l5 5.517 5-5.517" }),
12
+ link: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71" }), _jsx("path", { d: "M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71" })] })),
13
+ x: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M18 6L6 18" }), _jsx("path", { d: "M6 6l12 12" })] })),
14
+ 'arrow-right': (_jsxs(_Fragment, { children: [_jsx("path", { d: "M5 12h14" }), _jsx("path", { d: "M12 5l7 7-7 7" })] })),
15
+ 'info-circle': (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "M12 16v-4" }), _jsx("circle", { cx: "12", cy: "8", r: ".5" })] })),
16
+ raster: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M0 3C0 1.34315 1.34315 0 3 0H11V11H0V3Z" }), _jsx("path", { d: "M13 0H18.5C21.5376 0 24 2.46243 24 5.5V5.5C24 8.53757 21.5376 11 18.5 11H13V0Z" }), _jsx("path", { d: "M0 13H11V24H3C1.34315 24 0 22.6569 0 21V13Z" }), _jsx("path", { d: "M13 13H18C21.3137 13 24 15.6863 24 19V24H13V13Z" })] })),
17
+ };
18
+ export function Icon({ shape, solid = false, width = 14, }) {
19
+ return (_jsx("svg", { style: { width, flexShrink: 0 }, fill: solid ? 'currentColor' : 'none', stroke: solid ? undefined : 'currentColor', strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: solid ? undefined : 1.5, viewBox: "0 0 24 24", "aria-hidden": "true", children: SHAPES[shape] }));
20
+ }
21
+ //# sourceMappingURL=icons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.js","sourceRoot":"","sources":["../src/icons.tsx"],"names":[],"mappings":";AAEA,oGAAoG;AACpG,MAAM,MAAM,GAAG;IACd,MAAM,EAAE,CACP,8BACC,eAAM,CAAC,EAAC,sDAAsD,GAAG,EACjE,eAAM,CAAC,EAAC,gBAAgB,GAAG,IACzB,CACH;IACD,IAAI,EAAE,CACL,8BACC,eAAM,CAAC,EAAC,aAAa,GAAG,EACxB,eAAM,CAAC,EAAC,eAAe,GAAG,EAC1B,eAAM,CAAC,EAAC,gBAAgB,GAAG,EAC3B,eAAM,CAAC,EAAC,cAAc,GAAG,IACvB,CACH;IACD,KAAK,EAAE,eAAM,CAAC,EAAC,iBAAiB,GAAG;IACnC,MAAM,EAAE,iBAAQ,EAAE,EAAC,IAAI,EAAC,EAAE,EAAC,IAAI,EAAC,CAAC,EAAC,IAAI,GAAG;IACzC,MAAM,EAAE,CACP,8BACC,eAAM,CAAC,EAAC,2BAA2B,GAAG,EACtC,eAAM,CAAC,EAAC,iBAAiB,GAAG,EAC5B,eAAM,CAAC,EAAC,iBAAiB,GAAG,IAC1B,CACH;IACD,IAAI,EAAE,CACL,eAAM,CAAC,EAAC,8FAA8F,GAAG,CACzG;IACD,MAAM,EAAE,CACP,8BACC,eAAM,CAAC,EAAC,yCAAyC,GAAG,EACpD,eAAM,CAAC,EAAC,gBAAgB,GAAG,EAC3B,eAAM,CAAC,EAAC,UAAU,GAAG,IACnB,CACH;IACD,iBAAiB,EAAE,eAAM,CAAC,EAAC,gDAAgD,GAAG;IAC9E,IAAI,EAAE,CACL,8BACC,eAAM,CAAC,EAAC,0DAA0D,GAAG,EACrE,eAAM,CAAC,EAAC,2DAA2D,GAAG,IACpE,CACH;IACD,CAAC,EAAE,CACF,8BACC,eAAM,CAAC,EAAC,YAAY,GAAG,EACvB,eAAM,CAAC,EAAC,YAAY,GAAG,IACrB,CACH;IACD,aAAa,EAAE,CACd,8BACC,eAAM,CAAC,EAAC,UAAU,GAAG,EACrB,eAAM,CAAC,EAAC,eAAe,GAAG,IACxB,CACH;IACD,aAAa,EAAE,CACd,8BACC,iBAAQ,EAAE,EAAC,IAAI,EAAC,EAAE,EAAC,IAAI,EAAC,CAAC,EAAC,IAAI,GAAG,EACjC,eAAM,CAAC,EAAC,WAAW,GAAG,EACtB,iBAAQ,EAAE,EAAC,IAAI,EAAC,EAAE,EAAC,GAAG,EAAC,CAAC,EAAC,IAAI,GAAG,IAC9B,CACH;IACD,MAAM,EAAE,CACP,8BACC,eAAM,CAAC,EAAC,yCAAyC,GAAG,EACpD,eAAM,CAAC,EAAC,gFAAgF,GAAG,EAC3F,eAAM,CAAC,EAAC,6CAA6C,GAAG,EACxD,eAAM,CAAC,EAAC,iDAAiD,GAAG,IAC1D,CACH;CACmC,CAAA;AAErC,MAAM,UAAU,IAAI,CAAC,EACpB,KAAK,EACL,KAAK,GAAG,KAAK,EACb,KAAK,GAAG,EAAE,GAKV;IACA,OAAO,CACN,cACC,KAAK,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,EAAE,EAC/B,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,MAAM,EACrC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc,EAC1C,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,EACpC,OAAO,EAAC,WAAW,iBACP,MAAM,YAEjB,MAAM,CAAC,KAAK,CAAC,GACT,CACN,CAAA;AACF,CAAC","sourcesContent":["import type { ReactNode } from 'react'\n\n/** Raster's glyphs, path for path from raster/apps/web: 24-unit viewBox, 1.5 stroke, round caps. */\nconst SHAPES = {\n\tsearch: (\n\t\t<>\n\t\t\t<path d=\"M11 17.25a6.25 6.25 0 110-12.5 6.25 6.25 0 010 12.5z\" />\n\t\t\t<path d=\"M16 16l4.5 4.5\" />\n\t\t</>\n\t),\n\tgrid: (\n\t\t<>\n\t\t\t<path d=\"M3 3h7v7H3z\" />\n\t\t\t<path d=\"M14 3h7v7h-7z\" />\n\t\t\t<path d=\"M14 14h7v7h-7z\" />\n\t\t\t<path d=\"M3 14h7v7H3z\" />\n\t\t</>\n\t),\n\tcheck: <path d=\"M20 6L9 17l-5-5\" />,\n\tcircle: <circle cx=\"12\" cy=\"12\" r=\"10\" />,\n\tlayers: (\n\t\t<>\n\t\t\t<path d=\"M12 2L2 7l10 5 10-5-10-5z\" />\n\t\t\t<path d=\"M2 17l10 5 10-5\" />\n\t\t\t<path d=\"M2 12l10 5 10-5\" />\n\t\t</>\n\t),\n\tstar: (\n\t\t<path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\" />\n\t),\n\tupload: (\n\t\t<>\n\t\t\t<path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4\" />\n\t\t\t<path d=\"M17 8l-5-5-5 5\" />\n\t\t\t<path d=\"M12 3v12\" />\n\t\t</>\n\t),\n\t'chevron-up-down': <path d=\"M17 8.517L12 3 7 8.517M7 15.48l5 5.517 5-5.517\" />,\n\tlink: (\n\t\t<>\n\t\t\t<path d=\"M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71\" />\n\t\t\t<path d=\"M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71\" />\n\t\t</>\n\t),\n\tx: (\n\t\t<>\n\t\t\t<path d=\"M18 6L6 18\" />\n\t\t\t<path d=\"M6 6l12 12\" />\n\t\t</>\n\t),\n\t'arrow-right': (\n\t\t<>\n\t\t\t<path d=\"M5 12h14\" />\n\t\t\t<path d=\"M12 5l7 7-7 7\" />\n\t\t</>\n\t),\n\t'info-circle': (\n\t\t<>\n\t\t\t<circle cx=\"12\" cy=\"12\" r=\"10\" />\n\t\t\t<path d=\"M12 16v-4\" />\n\t\t\t<circle cx=\"12\" cy=\"8\" r=\".5\" />\n\t\t</>\n\t),\n\traster: (\n\t\t<>\n\t\t\t<path d=\"M0 3C0 1.34315 1.34315 0 3 0H11V11H0V3Z\" />\n\t\t\t<path d=\"M13 0H18.5C21.5376 0 24 2.46243 24 5.5V5.5C24 8.53757 21.5376 11 18.5 11H13V0Z\" />\n\t\t\t<path d=\"M0 13H11V24H3C1.34315 24 0 22.6569 0 21V13Z\" />\n\t\t\t<path d=\"M13 13H18C21.3137 13 24 15.6863 24 19V24H13V13Z\" />\n\t\t</>\n\t),\n} satisfies Record<string, ReactNode>\n\nexport function Icon({\n\tshape,\n\tsolid = false,\n\twidth = 14,\n}: {\n\tshape: keyof typeof SHAPES\n\tsolid?: boolean\n\twidth?: number\n}) {\n\treturn (\n\t\t<svg\n\t\t\tstyle={{ width, flexShrink: 0 }}\n\t\t\tfill={solid ? 'currentColor' : 'none'}\n\t\t\tstroke={solid ? undefined : 'currentColor'}\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\tstrokeWidth={solid ? undefined : 1.5}\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\taria-hidden=\"true\"\n\t\t>\n\t\t\t{SHAPES[shape]}\n\t\t</svg>\n\t)\n}\n"]}
@@ -0,0 +1,13 @@
1
+ export * from '@raster-app/sdk';
2
+ export { RasterProvider, type RasterProviderProps, useRasterClient } from './context.js';
3
+ export { type StateStore, localStorageStateStore, memoryStateStore } from './state-store.js';
4
+ export { type RasterSession, useSession } from './use-session.js';
5
+ export { type SignIn, type SignInState, useSignIn } from './use-sign-in.js';
6
+ export { useOrganizations } from './use-organizations.js';
7
+ export { type LibraryWithAccess, useLibraries } from './use-libraries.js';
8
+ export { type AssetsResource, useAssets } from './use-assets.js';
9
+ export { type AssetSearch, type SearchScope, useAssetSearch } from './use-asset-search.js';
10
+ export { type AssetUpload, type UploadState, useAssetUpload } from './use-asset-upload.js';
11
+ export { type PromoteState, type VariantPromote, usePromoteVariant } from './use-promote-variant.js';
12
+ export { type RasterNavigation, useRasterNavigation } from './use-navigation.js';
13
+ export { type RasterPickerProps, RasterPicker } from './raster-picker.js';
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ // A React host installs this package alone, so it carries the core's whole API.
2
+ export * from '@raster-app/sdk';
3
+ export { RasterProvider, useRasterClient } from './context.js';
4
+ export { localStorageStateStore, memoryStateStore } from './state-store.js';
5
+ export { useSession } from './use-session.js';
6
+ export { useSignIn } from './use-sign-in.js';
7
+ export { useOrganizations } from './use-organizations.js';
8
+ export { useLibraries } from './use-libraries.js';
9
+ export { useAssets } from './use-assets.js';
10
+ export { useAssetSearch } from './use-asset-search.js';
11
+ export { useAssetUpload } from './use-asset-upload.js';
12
+ export { usePromoteVariant } from './use-promote-variant.js';
13
+ export { useRasterNavigation } from './use-navigation.js';
14
+ export { RasterPicker } from './raster-picker.js';
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,cAAc,iBAAiB,CAAA;AAE/B,OAAO,EAAE,cAAc,EAA4B,eAAe,EAAE,MAAM,cAAc,CAAA;AACxF,OAAO,EAAmB,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAC5F,OAAO,EAAsB,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACjE,OAAO,EAAiC,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAA0B,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACzE,OAAO,EAAuB,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAChE,OAAO,EAAsC,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC1F,OAAO,EAAsC,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC1F,OAAO,EAA0C,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AACpG,OAAO,EAAyB,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAChF,OAAO,EAA0B,YAAY,EAAE,MAAM,oBAAoB,CAAA","sourcesContent":["// A React host installs this package alone, so it carries the core's whole API.\nexport * from '@raster-app/sdk'\n\nexport { RasterProvider, type RasterProviderProps, useRasterClient } from './context.js'\nexport { type StateStore, localStorageStateStore, memoryStateStore } from './state-store.js'\nexport { type RasterSession, useSession } from './use-session.js'\nexport { type SignIn, type SignInState, useSignIn } from './use-sign-in.js'\nexport { useOrganizations } from './use-organizations.js'\nexport { type LibraryWithAccess, useLibraries } from './use-libraries.js'\nexport { type AssetsResource, useAssets } from './use-assets.js'\nexport { type AssetSearch, type SearchScope, useAssetSearch } from './use-asset-search.js'\nexport { type AssetUpload, type UploadState, useAssetUpload } from './use-asset-upload.js'\nexport { type PromoteState, type VariantPromote, usePromoteVariant } from './use-promote-variant.js'\nexport { type RasterNavigation, useRasterNavigation } from './use-navigation.js'\nexport { type RasterPickerProps, RasterPicker } from './raster-picker.js'\n"]}
@@ -0,0 +1,71 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type Asset, type AssetVariant, type Organization, type RasterApiError, type SearchHit } from '@raster-app/sdk';
3
+ import type { PromoteState } from './use-promote-variant.js';
4
+ import type { UploadState } from './use-asset-upload.js';
5
+ export type ReportError = ((error: RasterApiError) => void) | undefined;
6
+ export declare function Brand(): import("react").JSX.Element;
7
+ export declare function ErrorCard({ error, onError }: {
8
+ error: RasterApiError;
9
+ onError: ReportError;
10
+ }): import("react").JSX.Element;
11
+ export declare function Empty({ glyph, title, text, children, }: {
12
+ glyph: 'grid' | '∅';
13
+ title: string;
14
+ text: string;
15
+ children?: ReactNode;
16
+ }): import("react").JSX.Element;
17
+ /**
18
+ * Raster's grid tile: the picture over its blurred fill, or a checkerboard where it has alpha,
19
+ * with a pill for a badge on the left and one for an action on the right.
20
+ */
21
+ export declare function Tile({ asset, onOpen, selected, badge, action, }: {
22
+ asset: Asset | SearchHit | AssetVariant;
23
+ onOpen: () => void;
24
+ /** Whether this is the version shown large, among an asset's versions. */
25
+ selected?: boolean;
26
+ badge?: ReactNode;
27
+ action?: ReactNode;
28
+ }): import("react").JSX.Element;
29
+ /**
30
+ * An opened asset or variant: the image large, its facts, and what the host can do with it.
31
+ * Raster opens through the host, which reaches the browser from a Figma plugin too.
32
+ */
33
+ export declare function Detail({ item, busy, onPick, }: {
34
+ item: Asset | SearchHit | AssetVariant;
35
+ /** While the asset is read in full for the host. */
36
+ busy: boolean;
37
+ onPick: () => void;
38
+ }): import("react").JSX.Element;
39
+ /** A file on its way into Raster, as the grid shows it until Raster has processed it. */
40
+ export declare function PendingTile({ name, preview, status, }: {
41
+ name: string;
42
+ preview: string;
43
+ status: string;
44
+ }): import("react").JSX.Element;
45
+ /** A control that opens the file chooser, drawn as whatever `className` makes it. */
46
+ export declare function FilePicker({ className, disabled, focusable, onFile, children, }: {
47
+ className: string;
48
+ disabled: boolean;
49
+ focusable?: boolean;
50
+ onFile: (file: File) => void;
51
+ children: ReactNode;
52
+ }): import("react").JSX.Element;
53
+ /** Where an upload and a promote stand, beside the header; each failure shows as an error. */
54
+ export declare function Progress({ upload, promote }: {
55
+ upload: UploadState;
56
+ promote: PromoteState;
57
+ }): import("react").JSX.Element;
58
+ /** Raster's organization switcher: the current organization, over a menu of every one. */
59
+ export declare function OrganizationSwitcher({ organizations, current, onIntent, onSelect, }: {
60
+ organizations: Array<Organization>;
61
+ current: Organization | undefined;
62
+ onIntent: (organization: Organization) => void;
63
+ onSelect: (organizationId: string) => void;
64
+ }): import("react").JSX.Element;
65
+ /**
66
+ * The spinner at the end of the grid, which asks for the next page once it is within two rows
67
+ * of view. Keyed by the item count, so it looks again after each page lands.
68
+ */
69
+ export declare function Sentinel({ onVisible }: {
70
+ onVisible: () => void;
71
+ }): import("react").JSX.Element;
@@ -0,0 +1,150 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { formatDateLong, formatFileSize, formatRelativeTime, getBlurhashPlaceholder, getPreviewUrl, getThumbUrl, isReady, toUserMessage, } from '@raster-app/sdk';
4
+ import { useRasterClient } from './context.js';
5
+ import { Icon } from './icons.js';
6
+ export function Brand() {
7
+ return (_jsxs("div", { className: "rs-brand", children: [_jsx(Icon, { shape: "raster", solid: true, width: 18 }), "Raster"] }));
8
+ }
9
+ // Strict Mode runs an effect twice in development; the host hears of each error once.
10
+ const reported = new WeakSet();
11
+ export function ErrorCard({ error, onError }) {
12
+ useEffect(() => {
13
+ if (reported.has(error))
14
+ return;
15
+ reported.add(error);
16
+ onError?.(error);
17
+ }, [error, onError]);
18
+ return (_jsx("p", { role: "alert", className: "rs-card", children: toUserMessage(error) }));
19
+ }
20
+ export function Empty({ glyph, title, text, children, }) {
21
+ return (_jsxs("div", { className: "rs-empty", children: [_jsx("div", { className: "rs-empty-glyph", children: glyph === 'grid' ? _jsx(Icon, { shape: "grid" }) : glyph }), _jsx("h2", { className: "rs-heading", children: title }), _jsx("p", { className: "rs-muted rs-small", children: text }), children] }));
22
+ }
23
+ /**
24
+ * Raster's grid tile: the picture over its blurred fill, or a checkerboard where it has alpha,
25
+ * with a pill for a badge on the left and one for an action on the right.
26
+ */
27
+ export function Tile({ asset, onOpen, selected, badge, action, }) {
28
+ const [loaded, setLoaded] = useState(false);
29
+ const ready = isReady(asset);
30
+ const thumb = getThumbUrl(asset);
31
+ const fill = ('blurhash' in asset ? getBlurhashPlaceholder(asset) : null) ?? thumb;
32
+ const transparent = 'contentType' in asset && /png|svg|webp|gif/.test(asset.contentType ?? '');
33
+ const size = 'size' in asset ? formatFileSize(asset.size) : '';
34
+ const updated = 'updated' in asset ? formatRelativeTime(asset.updated) : '';
35
+ return (_jsxs("div", { className: "rs-tile", children: [_jsxs("button", { type: "button", className: "rs-asset-thumb", "data-loaded": loaded || undefined, disabled: !ready, "aria-pressed": selected, title: 'created' in asset && asset.created
36
+ ? `Created ${formatDateLong(asset.created)}`
37
+ : undefined, onClick: onOpen, children: [transparent ? (_jsx("div", { className: "rs-transparent" })) : (fill && _jsx("img", { className: "rs-thumb-blur", src: fill, alt: "" })), _jsx("div", { className: "rs-thumb-preview", children: thumb && (_jsx("img", { src: thumb, alt: "", loading: "lazy", decoding: "async", onLoad: () => setLoaded(true), onError: () => setLoaded(true) }, thumb)) }), _jsxs("div", { className: "rs-thumb-meta", children: [_jsx("span", { className: "rs-thumb-title", children: asset.name }), _jsxs("span", { className: "rs-thumb-subtitle", children: [!ready && _jsx("span", { children: "Not ready" }), ready && size && _jsx("span", { children: size }), ready && updated && _jsx("span", { children: updated }), ready && !size && !updated && asset.width && asset.height && (_jsxs("span", { children: [asset.width, " \u00D7 ", asset.height] }))] })] })] }), badge, action] }));
38
+ }
39
+ /**
40
+ * An opened asset or variant: the image large, its facts, and what the host can do with it.
41
+ * Raster opens through the host, which reaches the browser from a Figma plugin too.
42
+ */
43
+ export function Detail({ item, busy, onPick, }) {
44
+ const { host } = useRasterClient();
45
+ const image = getPreviewUrl(item);
46
+ const appUrl = item.appUrl;
47
+ const facts = [
48
+ item.width && item.height ? `${item.width} × ${item.height}` : '',
49
+ 'size' in item ? formatFileSize(item.size) : '',
50
+ 'created' in item && item.created ? `Created ${formatDateLong(item.created)}` : '',
51
+ ].filter((fact) => fact !== '');
52
+ return (_jsxs("div", { className: "rs-detail", children: [_jsx("div", { className: "rs-detail-image", children: image && _jsx("img", { src: image, alt: "" }, image) }), _jsxs("div", { className: "rs-detail-info", children: [_jsx("h2", { className: "rs-heading", children: item.name }), facts.length > 0 && _jsx("p", { className: "rs-muted rs-small", children: facts.join(' · ') }), _jsx("button", { type: "button", className: "rs-button rs-button-primary", disabled: !isReady(item) || busy, onClick: onPick, children: "Pick asset" }), appUrl && (_jsxs("a", { className: "rs-button rs-button-secondary", href: appUrl, target: "_blank", rel: "noreferrer", onClick: (event) => {
53
+ event.preventDefault();
54
+ host.openExternal(appUrl);
55
+ }, children: [_jsx(Icon, { shape: "link", width: 12.25 }), "Open in Raster"] }))] })] }));
56
+ }
57
+ /** A file on its way into Raster, as the grid shows it until Raster has processed it. */
58
+ export function PendingTile({ name, preview, status, }) {
59
+ return (_jsx("div", { className: "rs-tile", children: _jsxs("div", { className: "rs-asset-thumb", "data-loaded": true, "aria-busy": "true", children: [_jsx("img", { className: "rs-thumb-blur", src: preview, alt: "" }), _jsx("div", { className: "rs-thumb-preview", children: _jsx("img", { src: preview, alt: "" }) }), _jsxs("div", { className: "rs-thumb-meta", children: [_jsx("span", { className: "rs-thumb-title", children: name }), _jsxs("span", { className: "rs-thumb-subtitle", children: [_jsx("span", { className: "rs-spinner" }), status] })] })] }) }));
60
+ }
61
+ /** A control that opens the file chooser, drawn as whatever `className` makes it. */
62
+ export function FilePicker({ className, disabled, focusable = true, onFile, children, }) {
63
+ return (_jsxs("label", { className: className, children: [children, _jsx("input", { className: "rs-visually-hidden", type: "file", disabled: disabled, tabIndex: focusable ? 0 : -1, onChange: (event) => {
64
+ const file = event.target.files?.[0];
65
+ // Cleared, so picking the same file again uploads it again.
66
+ event.target.value = '';
67
+ if (file)
68
+ onFile(file);
69
+ } })] }));
70
+ }
71
+ const UPLOADS = {
72
+ uploading: 'Uploading',
73
+ processing: 'Processing',
74
+ done: 'Uploaded',
75
+ 'timed-out': 'Still processing',
76
+ };
77
+ const PROMOTES = {
78
+ promoting: 'Setting as default',
79
+ done: 'Default set',
80
+ 'timed-out': 'Still setting as default',
81
+ };
82
+ /** Where an upload and a promote stand, beside the header; each failure shows as an error. */
83
+ export function Progress({ upload, promote }) {
84
+ return (_jsxs(_Fragment, { children: [upload.status !== 'idle' && upload.status !== 'error' && (_jsx(Status, { status: upload.status, text: UPLOADS[upload.status] })), promote.status !== 'idle' && promote.status !== 'error' && (_jsx(Status, { status: promote.status, text: PROMOTES[promote.status] }))] }));
85
+ }
86
+ function Status({ status, text }) {
87
+ let glyph = _jsx("span", { className: "rs-spinner" });
88
+ if (status === 'done')
89
+ glyph = _jsx(Icon, { shape: "check" });
90
+ if (status === 'timed-out')
91
+ glyph = _jsx(Icon, { shape: "info-circle" });
92
+ return (_jsxs("span", { className: "rs-status", "data-status": status, children: [glyph, text] }));
93
+ }
94
+ /** Raster's organization switcher: the current organization, over a menu of every one. */
95
+ export function OrganizationSwitcher({ organizations, current, onIntent, onSelect, }) {
96
+ const [open, setOpen] = useState(false);
97
+ const root = useRef(null);
98
+ useEffect(() => {
99
+ if (!open)
100
+ return;
101
+ const close = (event) => {
102
+ const outside = event.target instanceof Node && !root.current?.contains(event.target);
103
+ if (event instanceof KeyboardEvent ? event.key === 'Escape' : outside)
104
+ setOpen(false);
105
+ };
106
+ document.addEventListener('mousedown', close);
107
+ document.addEventListener('keydown', close);
108
+ return () => {
109
+ document.removeEventListener('mousedown', close);
110
+ document.removeEventListener('keydown', close);
111
+ };
112
+ }, [open]);
113
+ return (_jsxs("div", { className: "rs-org", ref: root, children: [_jsxs("button", { type: "button", className: "rs-sidebar-item rs-org-trigger", "aria-expanded": open, onClick: () => setOpen(!open), children: [_jsx(Avatar, { name: current?.name ?? current?.id ?? '' }), _jsx("span", { className: "rs-library-name", children: current?.name ?? current?.id }), _jsx(Icon, { shape: "chevron-up-down" })] }), open && (_jsxs("div", { className: "rs-menu", children: [_jsx("div", { className: "rs-menu-heading", children: "Organizations" }), organizations.map((organization) => (_jsxs("button", { type: "button", className: organization.id === current?.id ? 'rs-sidebar-item is-active' : 'rs-sidebar-item', onMouseEnter: () => onIntent(organization), onFocus: () => onIntent(organization), onClick: () => {
114
+ setOpen(false);
115
+ onSelect(organization.id);
116
+ }, children: [_jsx(Avatar, { name: organization.name ?? organization.id }), _jsx("span", { className: "rs-library-name", children: organization.name ?? organization.id })] }, organization.id)))] }))] }));
117
+ }
118
+ /** Raster's letter avatar, on a gradient hued from the name as the app hues it. */
119
+ function Avatar({ name }) {
120
+ if (name === '')
121
+ return null;
122
+ let hex = '';
123
+ for (let index = 0; index < Math.min(name.length, 10); index++) {
124
+ hex += `000${name.charCodeAt(index).toString(16)}`.slice(-4);
125
+ }
126
+ const hue = Number.parseInt(hex, 16) % 360;
127
+ return (_jsx("span", { className: "rs-avatar", style: {
128
+ backgroundImage: `linear-gradient(45deg, hsl(${hue}, 70%, 50%), hsl(${(hue + 60) % 360}, 70%, 50%))`,
129
+ }, children: name.charAt(0).toUpperCase() }));
130
+ }
131
+ /**
132
+ * The spinner at the end of the grid, which asks for the next page once it is within two rows
133
+ * of view. Keyed by the item count, so it looks again after each page lands.
134
+ */
135
+ export function Sentinel({ onVisible }) {
136
+ const spinner = useRef(null);
137
+ useEffect(() => {
138
+ const node = spinner.current;
139
+ if (node === null || typeof IntersectionObserver === 'undefined')
140
+ return;
141
+ const observer = new IntersectionObserver((entries) => {
142
+ if (entries.some((entry) => entry.isIntersecting))
143
+ onVisible();
144
+ }, { root: node.closest('.rs-content'), rootMargin: '0px 0px 360px 0px' });
145
+ observer.observe(node);
146
+ return () => observer.disconnect();
147
+ }, [onVisible]);
148
+ return _jsx("span", { ref: spinner, className: "rs-spinner" });
149
+ }
150
+ //# sourceMappingURL=picker-parts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"picker-parts.js","sourceRoot":"","sources":["../src/picker-parts.tsx"],"names":[],"mappings":"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","sourcesContent":["import { type ReactNode, useEffect, useRef, useState } from 'react'\n\nimport {\n\ttype Asset,\n\ttype AssetVariant,\n\ttype Organization,\n\ttype RasterApiError,\n\ttype SearchHit,\n\tformatDateLong,\n\tformatFileSize,\n\tformatRelativeTime,\n\tgetBlurhashPlaceholder,\n\tgetPreviewUrl,\n\tgetThumbUrl,\n\tisReady,\n\ttoUserMessage,\n} from '@raster-app/sdk'\n\nimport { useRasterClient } from './context.js'\nimport { Icon } from './icons.js'\nimport type { PromoteState } from './use-promote-variant.js'\nimport type { UploadState } from './use-asset-upload.js'\n\nexport type ReportError = ((error: RasterApiError) => void) | undefined\n\nexport function Brand() {\n\treturn (\n\t\t<div className=\"rs-brand\">\n\t\t\t<Icon shape=\"raster\" solid width={18} />\n\t\t\tRaster\n\t\t</div>\n\t)\n}\n\n// Strict Mode runs an effect twice in development; the host hears of each error once.\nconst reported = new WeakSet<RasterApiError>()\n\nexport function ErrorCard({ error, onError }: { error: RasterApiError; onError: ReportError }) {\n\tuseEffect(() => {\n\t\tif (reported.has(error)) return\n\t\treported.add(error)\n\t\tonError?.(error)\n\t}, [error, onError])\n\treturn (\n\t\t<p role=\"alert\" className=\"rs-card\">\n\t\t\t{toUserMessage(error)}\n\t\t</p>\n\t)\n}\n\nexport function Empty({\n\tglyph,\n\ttitle,\n\ttext,\n\tchildren,\n}: {\n\tglyph: 'grid' | '∅'\n\ttitle: string\n\ttext: string\n\tchildren?: ReactNode\n}) {\n\treturn (\n\t\t<div className=\"rs-empty\">\n\t\t\t<div className=\"rs-empty-glyph\">{glyph === 'grid' ? <Icon shape=\"grid\" /> : glyph}</div>\n\t\t\t<h2 className=\"rs-heading\">{title}</h2>\n\t\t\t<p className=\"rs-muted rs-small\">{text}</p>\n\t\t\t{children}\n\t\t</div>\n\t)\n}\n\n/**\n * Raster's grid tile: the picture over its blurred fill, or a checkerboard where it has alpha,\n * with a pill for a badge on the left and one for an action on the right.\n */\nexport function Tile({\n\tasset,\n\tonOpen,\n\tselected,\n\tbadge,\n\taction,\n}: {\n\tasset: Asset | SearchHit | AssetVariant\n\tonOpen: () => void\n\t/** Whether this is the version shown large, among an asset's versions. */\n\tselected?: boolean\n\tbadge?: ReactNode\n\taction?: ReactNode\n}) {\n\tconst [loaded, setLoaded] = useState(false)\n\tconst ready = isReady(asset)\n\tconst thumb = getThumbUrl(asset)\n\tconst fill = ('blurhash' in asset ? getBlurhashPlaceholder(asset) : null) ?? thumb\n\tconst transparent = 'contentType' in asset && /png|svg|webp|gif/.test(asset.contentType ?? '')\n\tconst size = 'size' in asset ? formatFileSize(asset.size) : ''\n\tconst updated = 'updated' in asset ? formatRelativeTime(asset.updated) : ''\n\n\treturn (\n\t\t<div className=\"rs-tile\">\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"rs-asset-thumb\"\n\t\t\t\tdata-loaded={loaded || undefined}\n\t\t\t\tdisabled={!ready}\n\t\t\t\taria-pressed={selected}\n\t\t\t\ttitle={\n\t\t\t\t\t'created' in asset && asset.created\n\t\t\t\t\t\t? `Created ${formatDateLong(asset.created)}`\n\t\t\t\t\t\t: undefined\n\t\t\t\t}\n\t\t\t\tonClick={onOpen}\n\t\t\t>\n\t\t\t\t{transparent ? (\n\t\t\t\t\t<div className=\"rs-transparent\" />\n\t\t\t\t) : (\n\t\t\t\t\tfill && <img className=\"rs-thumb-blur\" src={fill} alt=\"\" />\n\t\t\t\t)}\n\t\t\t\t<div className=\"rs-thumb-preview\">\n\t\t\t\t\t{thumb && (\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tkey={thumb}\n\t\t\t\t\t\t\tsrc={thumb}\n\t\t\t\t\t\t\talt=\"\"\n\t\t\t\t\t\t\tloading=\"lazy\"\n\t\t\t\t\t\t\tdecoding=\"async\"\n\t\t\t\t\t\t\tonLoad={() => setLoaded(true)}\n\t\t\t\t\t\t\tonError={() => setLoaded(true)}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t\t<div className=\"rs-thumb-meta\">\n\t\t\t\t\t<span className=\"rs-thumb-title\">{asset.name}</span>\n\t\t\t\t\t<span className=\"rs-thumb-subtitle\">\n\t\t\t\t\t\t{!ready && <span>Not ready</span>}\n\t\t\t\t\t\t{ready && size && <span>{size}</span>}\n\t\t\t\t\t\t{ready && updated && <span>{updated}</span>}\n\t\t\t\t\t\t{ready && !size && !updated && asset.width && asset.height && (\n\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t{asset.width} × {asset.height}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</button>\n\t\t\t{badge}\n\t\t\t{action}\n\t\t</div>\n\t)\n}\n\n/**\n * An opened asset or variant: the image large, its facts, and what the host can do with it.\n * Raster opens through the host, which reaches the browser from a Figma plugin too.\n */\nexport function Detail({\n\titem,\n\tbusy,\n\tonPick,\n}: {\n\titem: Asset | SearchHit | AssetVariant\n\t/** While the asset is read in full for the host. */\n\tbusy: boolean\n\tonPick: () => void\n}) {\n\tconst { host } = useRasterClient()\n\tconst image = getPreviewUrl(item)\n\tconst appUrl = item.appUrl\n\tconst facts = [\n\t\titem.width && item.height ? `${item.width} × ${item.height}` : '',\n\t\t'size' in item ? formatFileSize(item.size) : '',\n\t\t'created' in item && item.created ? `Created ${formatDateLong(item.created)}` : '',\n\t].filter((fact) => fact !== '')\n\n\treturn (\n\t\t<div className=\"rs-detail\">\n\t\t\t<div className=\"rs-detail-image\">{image && <img key={image} src={image} alt=\"\" />}</div>\n\t\t\t<div className=\"rs-detail-info\">\n\t\t\t\t<h2 className=\"rs-heading\">{item.name}</h2>\n\t\t\t\t{facts.length > 0 && <p className=\"rs-muted rs-small\">{facts.join(' · ')}</p>}\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclassName=\"rs-button rs-button-primary\"\n\t\t\t\t\tdisabled={!isReady(item) || busy}\n\t\t\t\t\tonClick={onPick}\n\t\t\t\t>\n\t\t\t\t\tPick asset\n\t\t\t\t</button>\n\t\t\t\t{appUrl && (\n\t\t\t\t\t<a\n\t\t\t\t\t\tclassName=\"rs-button rs-button-secondary\"\n\t\t\t\t\t\thref={appUrl}\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noreferrer\"\n\t\t\t\t\t\tonClick={(event) => {\n\t\t\t\t\t\t\tevent.preventDefault()\n\t\t\t\t\t\t\thost.openExternal(appUrl)\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Icon shape=\"link\" width={12.25} />\n\t\t\t\t\t\tOpen in Raster\n\t\t\t\t\t</a>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\n/** A file on its way into Raster, as the grid shows it until Raster has processed it. */\nexport function PendingTile({\n\tname,\n\tpreview,\n\tstatus,\n}: {\n\tname: string\n\tpreview: string\n\tstatus: string\n}) {\n\treturn (\n\t\t<div className=\"rs-tile\">\n\t\t\t<div className=\"rs-asset-thumb\" data-loaded aria-busy=\"true\">\n\t\t\t\t<img className=\"rs-thumb-blur\" src={preview} alt=\"\" />\n\t\t\t\t<div className=\"rs-thumb-preview\">\n\t\t\t\t\t<img src={preview} alt=\"\" />\n\t\t\t\t</div>\n\t\t\t\t<div className=\"rs-thumb-meta\">\n\t\t\t\t\t<span className=\"rs-thumb-title\">{name}</span>\n\t\t\t\t\t<span className=\"rs-thumb-subtitle\">\n\t\t\t\t\t\t<span className=\"rs-spinner\" />\n\t\t\t\t\t\t{status}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\n/** A control that opens the file chooser, drawn as whatever `className` makes it. */\nexport function FilePicker({\n\tclassName,\n\tdisabled,\n\tfocusable = true,\n\tonFile,\n\tchildren,\n}: {\n\tclassName: string\n\tdisabled: boolean\n\tfocusable?: boolean\n\tonFile: (file: File) => void\n\tchildren: ReactNode\n}) {\n\treturn (\n\t\t<label className={className}>\n\t\t\t{children}\n\t\t\t<input\n\t\t\t\tclassName=\"rs-visually-hidden\"\n\t\t\t\ttype=\"file\"\n\t\t\t\tdisabled={disabled}\n\t\t\t\ttabIndex={focusable ? 0 : -1}\n\t\t\t\tonChange={(event) => {\n\t\t\t\t\tconst file = event.target.files?.[0]\n\t\t\t\t\t// Cleared, so picking the same file again uploads it again.\n\t\t\t\t\tevent.target.value = ''\n\t\t\t\t\tif (file) onFile(file)\n\t\t\t\t}}\n\t\t\t/>\n\t\t</label>\n\t)\n}\n\nconst UPLOADS = {\n\tuploading: 'Uploading',\n\tprocessing: 'Processing',\n\tdone: 'Uploaded',\n\t'timed-out': 'Still processing',\n} as const\nconst PROMOTES = {\n\tpromoting: 'Setting as default',\n\tdone: 'Default set',\n\t'timed-out': 'Still setting as default',\n} as const\n\n/** Where an upload and a promote stand, beside the header; each failure shows as an error. */\nexport function Progress({ upload, promote }: { upload: UploadState; promote: PromoteState }) {\n\treturn (\n\t\t<>\n\t\t\t{upload.status !== 'idle' && upload.status !== 'error' && (\n\t\t\t\t<Status status={upload.status} text={UPLOADS[upload.status]} />\n\t\t\t)}\n\t\t\t{promote.status !== 'idle' && promote.status !== 'error' && (\n\t\t\t\t<Status status={promote.status} text={PROMOTES[promote.status]} />\n\t\t\t)}\n\t\t</>\n\t)\n}\n\nfunction Status({ status, text }: { status: string; text: string }) {\n\tlet glyph = <span className=\"rs-spinner\" />\n\tif (status === 'done') glyph = <Icon shape=\"check\" />\n\tif (status === 'timed-out') glyph = <Icon shape=\"info-circle\" />\n\treturn (\n\t\t<span className=\"rs-status\" data-status={status}>\n\t\t\t{glyph}\n\t\t\t{text}\n\t\t</span>\n\t)\n}\n\n/** Raster's organization switcher: the current organization, over a menu of every one. */\nexport function OrganizationSwitcher({\n\torganizations,\n\tcurrent,\n\tonIntent,\n\tonSelect,\n}: {\n\torganizations: Array<Organization>\n\tcurrent: Organization | undefined\n\tonIntent: (organization: Organization) => void\n\tonSelect: (organizationId: string) => void\n}) {\n\tconst [open, setOpen] = useState(false)\n\tconst root = useRef<HTMLDivElement>(null)\n\n\tuseEffect(() => {\n\t\tif (!open) return\n\t\tconst close = (event: MouseEvent | KeyboardEvent) => {\n\t\t\tconst outside = event.target instanceof Node && !root.current?.contains(event.target)\n\t\t\tif (event instanceof KeyboardEvent ? event.key === 'Escape' : outside) setOpen(false)\n\t\t}\n\t\tdocument.addEventListener('mousedown', close)\n\t\tdocument.addEventListener('keydown', close)\n\t\treturn () => {\n\t\t\tdocument.removeEventListener('mousedown', close)\n\t\t\tdocument.removeEventListener('keydown', close)\n\t\t}\n\t}, [open])\n\n\treturn (\n\t\t<div className=\"rs-org\" ref={root}>\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"rs-sidebar-item rs-org-trigger\"\n\t\t\t\taria-expanded={open}\n\t\t\t\tonClick={() => setOpen(!open)}\n\t\t\t>\n\t\t\t\t<Avatar name={current?.name ?? current?.id ?? ''} />\n\t\t\t\t<span className=\"rs-library-name\">{current?.name ?? current?.id}</span>\n\t\t\t\t<Icon shape=\"chevron-up-down\" />\n\t\t\t</button>\n\t\t\t{open && (\n\t\t\t\t<div className=\"rs-menu\">\n\t\t\t\t\t<div className=\"rs-menu-heading\">Organizations</div>\n\t\t\t\t\t{organizations.map((organization) => (\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tkey={organization.id}\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\t\torganization.id === current?.id ? 'rs-sidebar-item is-active' : 'rs-sidebar-item'\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tonMouseEnter={() => onIntent(organization)}\n\t\t\t\t\t\t\tonFocus={() => onIntent(organization)}\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tsetOpen(false)\n\t\t\t\t\t\t\t\tonSelect(organization.id)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Avatar name={organization.name ?? organization.id} />\n\t\t\t\t\t\t\t<span className=\"rs-library-name\">{organization.name ?? organization.id}</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t))}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n\n/** Raster's letter avatar, on a gradient hued from the name as the app hues it. */\nfunction Avatar({ name }: { name: string }) {\n\tif (name === '') return null\n\tlet hex = ''\n\tfor (let index = 0; index < Math.min(name.length, 10); index++) {\n\t\thex += `000${name.charCodeAt(index).toString(16)}`.slice(-4)\n\t}\n\tconst hue = Number.parseInt(hex, 16) % 360\n\treturn (\n\t\t<span\n\t\t\tclassName=\"rs-avatar\"\n\t\t\tstyle={{\n\t\t\t\tbackgroundImage: `linear-gradient(45deg, hsl(${hue}, 70%, 50%), hsl(${(hue + 60) % 360}, 70%, 50%))`,\n\t\t\t}}\n\t\t>\n\t\t\t{name.charAt(0).toUpperCase()}\n\t\t</span>\n\t)\n}\n\n/**\n * The spinner at the end of the grid, which asks for the next page once it is within two rows\n * of view. Keyed by the item count, so it looks again after each page lands.\n */\nexport function Sentinel({ onVisible }: { onVisible: () => void }) {\n\tconst spinner = useRef<HTMLSpanElement>(null)\n\n\tuseEffect(() => {\n\t\tconst node = spinner.current\n\t\tif (node === null || typeof IntersectionObserver === 'undefined') return\n\t\tconst observer = new IntersectionObserver(\n\t\t\t(entries) => {\n\t\t\t\tif (entries.some((entry) => entry.isIntersecting)) onVisible()\n\t\t\t},\n\t\t\t{ root: node.closest('.rs-content'), rootMargin: '0px 0px 360px 0px' }\n\t\t)\n\t\tobserver.observe(node)\n\t\treturn () => observer.disconnect()\n\t}, [onVisible])\n\n\treturn <span ref={spinner} className=\"rs-spinner\" />\n}\n"]}