@raster-app/react 0.0.0-stage → 0.1.0-canary-2e1834e

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 +136 -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 +56 -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,137 @@
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
+ React bindings for [`@raster-app/sdk`](https://www.npmjs.com/package/@raster-app/sdk): a drop-in
4
+ Raster asset picker, and the provider and hooks it is built on (session, organizations,
5
+ libraries, assets, search, uploads and the navigation model every Raster plugin shares). They
6
+ cache in TanStack Query.
7
+
8
+ ```sh
9
+ npm install @raster-app/react
10
+ ```
11
+
12
+ It takes the host's React, 18 or 19, as a peer dependency. It re-exports all of
13
+ `@raster-app/sdk`, so the client, `toUserMessage` and types such as `Asset` and `Library` come
14
+ from `@raster-app/react` too.
15
+
16
+ ## The picker
17
+
18
+ `RasterPicker` is Raster's asset picker, drawn in Raster's own design: sign-in with the device
19
+ grant or an API key, Raster's organization switcher, the organization's libraries, search, and
20
+ infinite scroll. A tile opens its asset large, with Pick asset and Open in Raster, which goes
21
+ through the host's `openExternal`, over the asset's versions, where a variant is uploaded or set as
22
+ the default. It uploads into a library from its menu or a drop. An organization or a library is
23
+ read as the pointer reaches it, so it opens at once. It fills its container.
24
+
25
+ ```tsx
26
+ import {
27
+ RasterPicker,
28
+ RasterProvider,
29
+ getFullSizeUrl,
30
+ localStorageCredentialStore,
31
+ } from '@raster-app/react'
32
+ import '@raster-app/react/styles.css'
33
+
34
+ function Picker({ onImage }: { onImage: (url: string) => void }) {
35
+ return (
36
+ <RasterProvider
37
+ config={{
38
+ host: { name: 'Sanity', openExternal: (url) => window.open(url, '_blank') },
39
+ credentials: localStorageCredentialStore(),
40
+ }}
41
+ >
42
+ <RasterPicker onPick={(asset) => onImage(getFullSizeUrl(asset) ?? '')} />
43
+ </RasterProvider>
44
+ )
45
+ }
46
+ ```
47
+
48
+ | Prop | What it does |
49
+ | ---------------- | --------------------------------------------------------------------------------------------------------------- |
50
+ | `onPick` | Called with the picked `Asset`, from any view, and its organization's id. `getFullSizeUrl(asset)` is the image. |
51
+ | `organizationId` | Binds the device sign-in to one organization: any other grant fails with `ORGANIZATION_MISMATCH`. |
52
+ | `scope` | The consent the device sign-in asks for: `'org'` when `organizationId` is set, `'user'` otherwise. |
53
+ | `onError` | Called once with each `RasterApiError` the picker shows, for the host to log. |
54
+ | `theme` | `'light'` or `'dark'`. The system's color scheme when unset. |
55
+
56
+ The styles are scoped under `.rs`, so they leave the host's own untouched.
57
+
58
+ ## Your own UI
59
+
60
+ A host that draws its own UI uses the hooks under the same provider.
61
+
62
+ ```tsx
63
+ import {
64
+ RasterProvider,
65
+ createRasterClient,
66
+ localStorageCredentialStore,
67
+ useAssets,
68
+ useLibraries,
69
+ useSession,
70
+ } from '@raster-app/react'
71
+
72
+ // Build the client once, outside render — a second one means a second session store.
73
+ const client = createRasterClient({
74
+ host: { name: 'Sanity', openExternal: (url) => window.open(url, '_blank') },
75
+ credentials: localStorageCredentialStore(),
76
+ })
77
+
78
+ function App() {
79
+ return (
80
+ <RasterProvider client={client}>
81
+ <Browser />
82
+ </RasterProvider>
83
+ )
84
+ }
85
+
86
+ function Browser() {
87
+ const { isConfigured } = useSession()
88
+ const libraries = useLibraries({ organizationId: 'acme' })
89
+ const assets = useAssets({ organizationId: 'acme', libraryId: 'lib-1' })
90
+
91
+ if (!isConfigured) return <SignIn />
92
+ return <Grid assets={assets.assets ?? []} onLoadMore={assets.loadMore} />
93
+ }
94
+ ```
95
+
96
+ ## Provider
97
+
98
+ | Prop | What it does |
99
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
100
+ | `client` or `config` | An existing `RasterClient`, or the config for one the provider creates. |
101
+ | `queryClient` | The host's own TanStack `QueryClient`, from the same TanStack Query major as this package. Without one the provider creates its own, which shadows the host's for everything below. |
102
+ | `state` | The `StateStore` where `useRasterNavigation` remembers the last organization and library: `memoryStateStore()` (the default), `localStorageStateStore()`, or the host's own. |
103
+
104
+ ## Hooks
105
+
106
+ | Hook | What it gives you |
107
+ | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
108
+ | `useSession()` | The credential, `isConfigured` / `isSignedIn`, `userName`, `signOut`. |
109
+ | `useSignIn()` | Device grant or API key. One state machine for both. Takes `scope` and `organizationId` for the device grant. |
110
+ | `useOrganizations()` | Everything the credential reaches, from `/me`, as a TanStack query result. |
111
+ | `useLibraries({ organizationId })` | The libraries in one organization, as a query result. Each carries `access`: `'write'`, `'read'` or `null` for none. |
112
+ | `useAssets({ organizationId, libraryId })` | A library's assets, paged. |
113
+ | `useAssetSearch(scope, query)` | Debounced, paged search that keeps the last results up while the next query runs. |
114
+ | `useAssetUpload()` | Upload, then poll until Raster has finished processing. |
115
+ | `usePromoteVariant()` | Promote a variant to its asset's default, then poll until Raster has swapped the files. Takes an optimistic update. |
116
+ | `useRasterNavigation({ organizations })` | `organization → library → asset`, persisted; `refresh()` refetches every Raster query. |
117
+
118
+ ## Things worth knowing
119
+
120
+ - **The provider fixes its client on mount.** An inline `config` builds one client for the
121
+ provider's life, and a later `client` prop is ignored: remount with a new `key` to switch.
122
+ Two clients for one credential would each refresh it, spending each other's refresh token.
123
+ - **`credentials` is `undefined` while the store is read.** Gate the sign-in screen on
124
+ `isConfigured` once it has settled, or it flashes on every launch.
125
+ - **Every fetch aborts when superseded**, which frees its connection for the requests that
126
+ follow.
127
+ - **The cache belongs to a sign-in.** Query keys carry the session's `credentialIdentity()`,
128
+ and a sign-out or a different account drops the previous one's queries. For an API key that
129
+ identity is a digest of the key, so the key itself stays out of the cache.
130
+ - **Errors are `RasterApiError`s.** Branch on `code`, and show `toUserMessage(error)`.
131
+ - **Raster finishes uploads and promotes after answering.** Both hooks poll until the work
132
+ lands, and settle as `timed-out` when it outlasts the wait: the work is still running, and a
133
+ later refetch shows it.
134
+
135
+ ## Preact
136
+
137
+ To use the hooks with Preact, alias `react` and `react-dom` to `preact/compat` in the bundler.
@@ -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