Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
# @suaveplan/drive-react-sharing - React UI for @suaveplan/drive-sharing: ShareDialog, PartitionBadge, FileAccessList components and useSharing hook wired to the real sharing engine.
|
||||
|
||||
## Summary
|
||||
|
||||
React-tier UI surface for `@suaveplan/drive-sharing`. Three controlled
|
||||
components plus a real-engine-backed hook.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [file-access-list](./src/components/file-access-list/file-access-list.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [file-access-list](./src/components/file-access-list/file-access-list.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [partition-badge](./src/components/partition-badge/partition-badge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [partition-badge](./src/components/partition-badge/partition-badge.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [share-dialog](./src/components/share-dialog/share-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [share-dialog](./src/components/share-dialog/share-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [use-sharing](./src/hooks/use-sharing/use-sharing.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [use-sharing](./src/hooks/use-sharing/use-sharing.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [error-codes](./src/error-codes.md) — `@suaveplan/drive-react-sharing` raises every failure as a `DriveReactSharingError` — a `SuaveplanError` subclass with…
|
||||
<!-- end:auto-docs --> - [file-access-list](./src/components/file-access-list/file-access-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-badge](./src/components/partition-badge/partition-badge.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-badge](./src/components/partition-badge/partition-badge.md) — <!-- begin:auto-docs --> _(no submodules)_- [share-dialog](./src/components/share-dialog/share-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- [share-dialog](./src/components/share-dialog/share-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-sharing](./src/hooks/use-sharing/use-sharing.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-sharing](./src/hooks/use-sharing/use-sharing.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — `@suaveplan/drive-react-sharing` raises every failure as a `DriveReactSharingError` — a `SuaveplanError` subclass with…
|
||||
- [file-access-list](./src/components/file-access-list/file-access-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-badge](./src/components/partition-badge/partition-badge.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-badge](./src/components/partition-badge/partition-badge.md) — <!-- begin:auto-docs --> _(no submodules)_- [share-dialog](./src/components/share-dialog/share-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- [share-dialog](./src/components/share-dialog/share-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-sharing](./src/hooks/use-sharing/use-sharing.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-sharing](./src/hooks/use-sharing/use-sharing.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — `@suaveplan/drive-react-sharing` raises every failure as a `DriveReactSharingError` — a `SuaveplanError` subclass with…
|
||||
- [file-access-list](./src/components/file-access-list/file-access-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-badge](./src/components/partition-badge/partition-badge.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-badge](./src/components/partition-badge/partition-badge.md) — <!-- begin:auto-docs --> _(no submodules)_- [share-dialog](./src/components/share-dialog/share-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- [share-dialog](./src/components/share-dialog/share-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-sharing](./src/hooks/use-sharing/use-sharing.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-sharing](./src/hooks/use-sharing/use-sharing.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — `@suaveplan/drive-react-sharing` raises every failure as a `DriveReactSharingError` — a `SuaveplanError` subclass with…
|
||||
- [file-access-list](./src/components/file-access-list/file-access-list.md) — <!-- begin:auto-docs --> _(no submodules)_- [partition-badge](./src/components/partition-badge/partition-badge.md) — <!-- begin:auto-docs --> _(no submodules)_ - [partition-badge](./src/components/partition-badge/partition-badge.md) — <!-- begin:auto-docs --> _(no submodules)_- [share-dialog](./src/components/share-dialog/share-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_ - [share-dialog](./src/components/share-dialog/share-dialog.md) — <!-- begin:auto-docs --> _(no submodules)_- [use-sharing](./src/hooks/use-sharing/use-sharing.md) — <!-- begin:auto-docs --> _(no submodules)_ - [use-sharing](./src/hooks/use-sharing/use-sharing.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ - [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — `@suaveplan/drive-react-sharing` raises every failure as a `DriveReactSharingError` — a `SuaveplanError` subclass with…
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/drive-react-sharing @suaveplan/drive-sharing
|
||||
```
|
||||
|
||||
Peer deps: `react ^18 || ^19`, `react-dom ^18 || ^19`.
|
||||
|
||||
## Quick start
|
||||
|
||||
```tsx
|
||||
import {
|
||||
GrantManager,
|
||||
InMemoryPartitionStore,
|
||||
KeyRing,
|
||||
PartitionManager,
|
||||
TokenManager,
|
||||
userId,
|
||||
} from "@suaveplan/drive-sharing";
|
||||
import {
|
||||
ShareDialog,
|
||||
useSharing,
|
||||
} from "@suaveplan/drive-react-sharing";
|
||||
import { useState } from "react";
|
||||
|
||||
const partitions = new PartitionManager({
|
||||
store: new InMemoryPartitionStore(),
|
||||
keyRing: new KeyRing(),
|
||||
});
|
||||
const grants = new GrantManager();
|
||||
const tokens = new TokenManager();
|
||||
|
||||
function Trigger({ partitionId }: { partitionId: string }) {
|
||||
const sharing = useSharing({
|
||||
partitionId,
|
||||
grantorId: userId("me"),
|
||||
partitions,
|
||||
grants,
|
||||
tokens,
|
||||
});
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={() => setOpen(true)}>Share</button>
|
||||
<ShareDialog
|
||||
open={open}
|
||||
recipients={sharing.recipients}
|
||||
loading={sharing.loading}
|
||||
errorMessage={sharing.error?.message}
|
||||
onAdd={(id, perm) => sharing.addRecipient(id, perm)}
|
||||
onRemove={(id) => sharing.removeRecipient(id)}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Purpose | Docs |
|
||||
|---|---|---|
|
||||
| `<ShareDialog />` | Modal share UI with add form + revoke per row | `src/components/share-dialog/share-dialog.md` |
|
||||
| `<PartitionBadge />` | Inline badge: classification + encryption indicator | `src/components/partition-badge/partition-badge.md` |
|
||||
| `<FileAccessList />` | Read-only access list with optional revoke | `src/components/file-access-list/file-access-list.md` |
|
||||
|
||||
## Hook
|
||||
|
||||
| Hook | Purpose | Docs |
|
||||
|---|---|---|
|
||||
| `useSharing(options)` | Live-engine binding for recipient state + add/remove/update/refresh actions | `src/hooks/use-sharing/use-sharing.md` |
|
||||
|
||||
## Accessibility
|
||||
|
||||
- `<ShareDialog />` renders `role="dialog"` with `aria-modal="true"`
|
||||
and `aria-labelledby` pointing at the title heading. Focus moves to
|
||||
the recipient input on open; ESC and backdrop click both close.
|
||||
- `<FileAccessList />` renders a semantic `<ul role="list">` with one
|
||||
`<li role="listitem">` per recipient. Revoke is a native `<button>`
|
||||
with `aria-label` including the recipient identifier. Delete and
|
||||
Backspace fire revoke directly when the button is focused.
|
||||
- `<PartitionBadge />` renders `role="status"` and an `aria-label`
|
||||
describing the partition type plus encryption state. The padlock
|
||||
SVG is marked `aria-hidden="true"`.
|
||||
|
||||
## OTel
|
||||
|
||||
`useSharing` emits three counters:
|
||||
|
||||
- `drive.react.sharing.recipients_added_total{permission}`
|
||||
- `drive.react.sharing.recipients_removed_total`
|
||||
- `drive.react.sharing.permissions_updated_total{permission}`
|
||||
|
||||
## Type consolidation
|
||||
|
||||
Main shipped three overlapping permission aliases
|
||||
(`SharePermission` / `AccessPermission` / `SharingPermission`).
|
||||
imports `SharePermission` directly from
|
||||
`@suaveplan/drive-sharing` and re-exports it as the single canonical
|
||||
name. Aliases were not ported.
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# `<FileAccessList />`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`<FileAccessList />` renders a read-only list of share recipients with
|
||||
an optional revoke action per row. It is the lightweight surface used
|
||||
inside file detail panels and partition sidebars where the full
|
||||
`<ShareDialog />` would be overkill. The component is purely
|
||||
presentational — recipient data flows in via `props.recipients` and
|
||||
mutations exit via the `onRevoke(grantId)` callback. There is no
|
||||
internal state, no engine dependency, and no async lifecycle.
|
||||
|
||||
## Features
|
||||
|
||||
- Renders one `<li>` per recipient with `data-grant-id` and
|
||||
`data-revoked` attributes for external styling and scripting.
|
||||
- Permission label rendered from the `primaryPermission` projection
|
||||
computed by `useSharing` (no recompute here).
|
||||
- Optional `onRevoke` — when omitted, the revoke button is hidden and
|
||||
the component is strictly read-only.
|
||||
- Keyboard accessibility: revoke button is a native `<button>`, plus
|
||||
Delete / Backspace shortcuts trigger revoke when the button is
|
||||
focused.
|
||||
- Empty state: when `recipients` is empty, renders a polite
|
||||
`aria-live="polite"` status message rather than an empty list.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```tsx
|
||||
import { FileAccessList } from "@suaveplan/drive-react-sharing";
|
||||
|
||||
<FileAccessList recipients={sharing.recipients} />;
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```tsx
|
||||
function Panel() {
|
||||
const sharing = useSharing(/* ... */);
|
||||
return (
|
||||
<FileAccessList
|
||||
recipients={sharing.recipients}
|
||||
onRevoke={(id) => sharing.removeRecipient(id)}
|
||||
revokeLabel="Remove"
|
||||
emptyLabel="No collaborators yet"
|
||||
listLabel="Document collaborators"
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `FileAccessListProps`:
|
||||
- `recipients: readonly ShareRecipient[]` — required.
|
||||
- `onRevoke?: (grantId: string) => void` — optional revoke callback.
|
||||
- `revokeLabel?: string` — defaults to `"Revoke"`.
|
||||
- `emptyLabel?: string` — defaults to `"No recipients"`.
|
||||
- `listLabel?: string` — defaults to `"Recipients with access"`.
|
||||
- `className?: string`, `style?: CSSProperties` — root overrides.
|
||||
- `FileAccessList(props): ReactElement` — pure functional component.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The component intentionally renders a `<button>` rather than a
|
||||
right-click context menu so revoke is reachable via keyboard alone.
|
||||
The Delete / Backspace shortcuts fire `onRevoke` directly without
|
||||
requiring a confirmation step — consumers that need a confirmation
|
||||
flow should compose their own modal in front of the callback.
|
||||
@@ -0,0 +1,69 @@
|
||||
# `<PartitionBadge />`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`<PartitionBadge />` renders a compact label visualising a partition's
|
||||
classification — `personal`, `shared`, `public`, or `encrypted` — plus
|
||||
an optional padlock indicator when the partition is encrypted. It is a
|
||||
pure presentational component with no engine dependencies, suitable for
|
||||
list rows, sidebar headers, and breadcrumbs.
|
||||
|
||||
## Features
|
||||
|
||||
- Four partition classifications matching the engine's `PartitionType`
|
||||
union.
|
||||
- Automatic padlock SVG when `type === "encrypted"` or `encrypted` prop
|
||||
is explicit-true.
|
||||
- Inline-styled by default so the component drops into any tree without
|
||||
a stylesheet. `className` / `style` props override.
|
||||
- Accessibility: `role="status"`, `aria-label` mentioning the type and
|
||||
encryption state, decorative SVG marked `aria-hidden="true"`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```tsx
|
||||
import { PartitionBadge } from "@suaveplan/drive-react-sharing";
|
||||
|
||||
<PartitionBadge type="shared" />;
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```tsx
|
||||
<PartitionBadge
|
||||
type="encrypted"
|
||||
label="E2E"
|
||||
className="my-badge"
|
||||
style={{ marginInlineStart: 12 }}
|
||||
/>
|
||||
```
|
||||
|
||||
`encrypted` forces the lock icon even when the type is something other
|
||||
than `encrypted` — useful for representing a shared partition whose
|
||||
policy mandates encryption.
|
||||
|
||||
```tsx
|
||||
<PartitionBadge type="shared" encrypted />
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PartitionBadgeProps`:
|
||||
- `type: PartitionType` — required, drives label + colour.
|
||||
- `encrypted?: boolean` — optional, forces lock icon.
|
||||
- `label?: string` — optional, overrides the default text.
|
||||
- `className?: string` — applied to root `<span>`.
|
||||
- `style?: CSSProperties` — merged onto default style.
|
||||
- `PartitionBadge(props): ReactElement` — pure functional component.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Colours are hard-coded HEX values with a 10% alpha background, chosen
|
||||
to ensure 4.5:1 WCAG contrast against the colour foreground. Consumers
|
||||
that require theme-aware colouring should pass an explicit `style`
|
||||
override rather than monkey-patching the defaults. The `data-partition-type`
|
||||
attribute is set so external stylesheets can target each variant
|
||||
without depending on the inline `style` prop.
|
||||
@@ -0,0 +1,108 @@
|
||||
# `<ShareDialog />`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`<ShareDialog />` renders the modal sharing UI for a partition. It
|
||||
exposes a recipients list, an add-recipient form (text input plus
|
||||
permission select), and a revoke button per row. The component is
|
||||
controlled — `open`, `recipients`, `loading`, and `errorMessage` are
|
||||
all owned by the parent. Wire it to `useSharing` for live engine
|
||||
integration or hand it static props in Storybook.
|
||||
|
||||
## Features
|
||||
|
||||
- Controlled `open` prop — renders `null` when closed so callers can
|
||||
conditionally mount.
|
||||
- Focus moves to the recipient input on open.
|
||||
- ESC key closes the dialog. Backdrop click also closes; clicks
|
||||
inside the dialog do NOT close (stopPropagation).
|
||||
- ARIA: `role="dialog"`, `aria-modal="true"`, `aria-labelledby` tied to
|
||||
the title heading. Error region uses `role="alert"`.
|
||||
- Recipient list reuses the same `data-grant-id` row contract as
|
||||
`<FileAccessList />`.
|
||||
- Localisable: pass `labels` to override every string. Permission
|
||||
options localise individually.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```tsx
|
||||
import { ShareDialog } from "@suaveplan/drive-react-sharing";
|
||||
import { useState } from "react";
|
||||
|
||||
function Trigger() {
|
||||
const sharing = useSharing(/* ... */);
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={() => setOpen(true)}>Share</button>
|
||||
<ShareDialog
|
||||
open={open}
|
||||
recipients={sharing.recipients}
|
||||
onAdd={(id, p) => sharing.addRecipient(id, p)}
|
||||
onRemove={(id) => sharing.removeRecipient(id)}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Custom labels for a localised application:
|
||||
|
||||
```tsx
|
||||
<ShareDialog
|
||||
open={open}
|
||||
recipients={sharing.recipients}
|
||||
loading={sharing.loading}
|
||||
errorMessage={sharing.error?.message}
|
||||
defaultPermission="write"
|
||||
onAdd={(id, p) => sharing.addRecipient(id, p)}
|
||||
onRemove={(id) => sharing.removeRecipient(id)}
|
||||
onClose={() => setOpen(false)}
|
||||
labels={{
|
||||
title: "Partager",
|
||||
add: "Ajouter",
|
||||
revoke: "Révoquer",
|
||||
empty: "Aucun destinataire",
|
||||
permissionOptions: {
|
||||
read: "Lecture",
|
||||
write: "Écriture",
|
||||
delete: "Suppression",
|
||||
share: "Partage",
|
||||
admin: "Administrateur",
|
||||
},
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ShareDialogProps`:
|
||||
- `open: boolean` — required.
|
||||
- `recipients: readonly ShareRecipient[]` — required.
|
||||
- `onAdd(recipientId, permission): void | Promise<void>` — required.
|
||||
- `onRemove(grantId): void | Promise<void>` — required.
|
||||
- `onClose(): void` — required.
|
||||
- `title?: string` — overrides `labels.title`.
|
||||
- `loading?: boolean` — disables interactive controls.
|
||||
- `errorMessage?: string` — renders `role="alert"` banner.
|
||||
- `defaultPermission?: SharePermission` — initial select value.
|
||||
- `labels?: Partial<ShareDialogLabels>` — string overrides.
|
||||
- `ShareDialogLabels` — exported interface; permission options
|
||||
override individually.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The dialog is intentionally a controlled component because the open
|
||||
state is usually tied to other UI in the parent (an avatar stack, a
|
||||
"Share" button, a routing flag). Storybook can drive the entire surface
|
||||
with mocked callbacks. The error region is rendered only when
|
||||
`errorMessage` is a non-empty string — passing an undefined or
|
||||
zero-length string hides it. The submit button is disabled when the
|
||||
recipient draft is empty so users cannot accidentally fire `onAdd`
|
||||
with whitespace.
|
||||
@@ -0,0 +1,84 @@
|
||||
# `drive-react-sharing` error codes
|
||||
|
||||
## Purpose
|
||||
|
||||
`@suaveplan/drive-react-sharing` raises every failure as a
|
||||
`DriveReactSharingError` — a `SuaveplanError` subclass with a stable code
|
||||
registry. Codes are namespaced under `DRIVE_REACT_SHARING.*` so consumers
|
||||
can write exhaustive `switch (err.code) { ... }` matches at the call site
|
||||
without depending on the underlying `@suaveplan/drive-sharing` engine
|
||||
codes. The actual engine error (or any other thrown value) is preserved
|
||||
on `cause` so a chain walker can still surface low-level diagnostics.
|
||||
|
||||
## Features
|
||||
|
||||
- Module-load-time registration with the global Suaveplan code registry.
|
||||
- Brand symbol on every instance — defence-in-depth against cross-realm
|
||||
`instanceof` failures via `DriveReactSharingError.is(value)`.
|
||||
- Seven codes covering input validation (`INVALID_RECIPIENT`), engine
|
||||
authorization failures (`PERMISSION_DENIED`, `GRANT_NOT_FOUND`), and
|
||||
hook action wrapping (`ADD_FAILED`, `REVOKE_FAILED`, `UPDATE_FAILED`,
|
||||
`REFRESH_FAILED`).
|
||||
- Compatible with the `SuaveplanError.cause` chain so the original
|
||||
`DriveShareError` thrown by the engine is recoverable.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
DriveReactSharingError,
|
||||
DRIVE_REACT_SHARING_CODES,
|
||||
} from "@suaveplan/drive-react-sharing";
|
||||
|
||||
try {
|
||||
await result.addRecipient("", "read");
|
||||
} catch (err) {
|
||||
if (DriveReactSharingError.is(err)) {
|
||||
if (err.code === DRIVE_REACT_SHARING_CODES.INVALID_RECIPIENT) {
|
||||
// Show inline form validation error
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Wrapping an engine-level error and walking the cause chain:
|
||||
|
||||
```ts
|
||||
import {
|
||||
DriveReactSharingError,
|
||||
DRIVE_REACT_SHARING_CODES,
|
||||
} from "@suaveplan/drive-react-sharing";
|
||||
import { DriveShareError } from "@suaveplan/drive-sharing";
|
||||
|
||||
try {
|
||||
await result.addRecipient("user-42", "admin");
|
||||
} catch (err) {
|
||||
if (
|
||||
DriveReactSharingError.is(err) &&
|
||||
err.code === DRIVE_REACT_SHARING_CODES.ADD_FAILED &&
|
||||
err.cause instanceof DriveShareError
|
||||
) {
|
||||
console.error("engine code:", err.cause.code);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `DRIVE_REACT_SHARING_CODES` — frozen record of every code literal.
|
||||
- `DriveReactSharingErrorCode` — union type of every code value.
|
||||
- `class DriveReactSharingError extends SuaveplanError<DriveReactSharingErrorCode>`
|
||||
- `new DriveReactSharingError(code, messageKey, options?)` — `options.cause`
|
||||
preserves the original error.
|
||||
- `static is(value): value is DriveReactSharingError` — brand-aware guard.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The brand symbol is unique per module evaluation; consumers MUST NOT
|
||||
construct a `DriveReactSharingError` outside this package (the
|
||||
constructor is exported only to allow shared error helpers in this
|
||||
workspace to throw it). The seven codes are intentionally split into
|
||||
input-validation versus action-wrapper buckets so UI can render
|
||||
field-level messages distinct from optimistic-update rollback toasts.
|
||||
@@ -0,0 +1,126 @@
|
||||
# `useSharing` hook
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`useSharing` is the React-tier binding for `@suaveplan/drive-sharing`. It
|
||||
holds a local list of `ShareRecipient` rows that mirror the active grants
|
||||
on a partition, and exposes four async actions (`addRecipient`,
|
||||
`removeRecipient`, `updatePermission`, `refresh`) that delegate to the
|
||||
real engine — no in-memory stub, no mocks. After every successful action
|
||||
the hook re-syncs from `GrantManager.list({ partitionId, active: true })`
|
||||
so optimistic UI never drifts from the engine's truth.
|
||||
|
||||
## Features
|
||||
|
||||
- Wires directly to live `PartitionManager`, `GrantManager`, and
|
||||
`TokenManager` instances supplied by the caller. The package does not
|
||||
own a partition store.
|
||||
- AbortSignal threaded through every action. A pre-aborted signal raises
|
||||
a `DriveReactSharingError` before any engine mutation.
|
||||
- Surfaces failures via both `result.error` (for inline rendering) and
|
||||
`throw` (for promise-chain orchestration). Underlying engine errors
|
||||
are preserved on `cause`.
|
||||
- Emits three OTel counters: `drive.react.sharing.recipients_added_total`,
|
||||
`drive.react.sharing.recipients_removed_total`,
|
||||
`drive.react.sharing.permissions_updated_total{permission}`.
|
||||
- Empty-string recipient ids raise `INVALID_RECIPIENT` synchronously.
|
||||
- Permission update is a revoke-then-create round-trip because the engine
|
||||
has no atomic mutate API on `GrantManager`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```tsx
|
||||
import {
|
||||
GrantManager,
|
||||
InMemoryPartitionStore,
|
||||
KeyRing,
|
||||
PartitionManager,
|
||||
TokenManager,
|
||||
userId,
|
||||
} from "@suaveplan/drive-sharing";
|
||||
import { useSharing } from "@suaveplan/drive-react-sharing";
|
||||
|
||||
const partitions = new PartitionManager({
|
||||
store: new InMemoryPartitionStore(),
|
||||
keyRing: new KeyRing(),
|
||||
});
|
||||
const grants = new GrantManager();
|
||||
const tokens = new TokenManager();
|
||||
|
||||
function PanelWidget({ partitionId }: { partitionId: string }) {
|
||||
const sharing = useSharing({
|
||||
partitionId,
|
||||
grantorId: userId("alice"),
|
||||
partitions,
|
||||
grants,
|
||||
tokens,
|
||||
});
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => sharing.addRecipient("bob", "read")}
|
||||
>
|
||||
Add bob
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Composing with `AbortController` so a parent dialog can cancel inflight
|
||||
mutations on close:
|
||||
|
||||
```tsx
|
||||
import { useEffect } from "react";
|
||||
|
||||
function Composer({
|
||||
partitionId,
|
||||
sharing,
|
||||
}: {
|
||||
partitionId: string;
|
||||
sharing: ReturnType<typeof useSharing>;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const ctrl = new AbortController();
|
||||
return () => ctrl.abort();
|
||||
}, [partitionId]);
|
||||
|
||||
async function bulkPromote(ids: readonly string[]): Promise<void> {
|
||||
const ctrl = new AbortController();
|
||||
for (const id of ids) {
|
||||
await sharing.updatePermission(id, "admin", ctrl.signal);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `useSharing(options: UseSharingOptions): UseSharingResult`
|
||||
- `options.partitionId: string` — partition the grants belong to.
|
||||
- `options.grantorId: UserId` — typically the current user.
|
||||
- `options.partitions: PartitionManager` — engine partition manager.
|
||||
- `options.grants: GrantManager` — engine grant manager.
|
||||
- `options.tokens: TokenManager` — engine token manager.
|
||||
- `options.initialRecipients?: readonly ShareRecipient[]` — optional
|
||||
SSR / Storybook seed; when omitted the hook seeds from
|
||||
`grants.list({ partitionId, active: true })`.
|
||||
- Returns `UseSharingResult` — `{ recipients, loading, error,
|
||||
addRecipient(id, perm, signal?), removeRecipient(grantId, signal?),
|
||||
updatePermission(grantId, perm, signal?), refresh(signal?) }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The recipient list is refreshed by re-reading from the engine after
|
||||
every mutation rather than spliced in place. This keeps the hook free
|
||||
of merge-conflict logic when multiple components share the same
|
||||
`GrantManager`. `primaryPermission` on each row is computed at projection
|
||||
time from a fixed tier ranking (`read < write < delete < share <
|
||||
admin`); consumers should still consult the full `permissions` array
|
||||
when making entitlement decisions, but UI badges can render the primary
|
||||
tier directly.
|
||||
@@ -0,0 +1,75 @@
|
||||
# OTel instruments
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`@suaveplan/drive-react-sharing` emits three counters covering every
|
||||
mutation the `useSharing` hook can dispatch. They are useful both for
|
||||
operational dashboards (how often are admins adding versus removing
|
||||
recipients?) and for product analytics (which permission tier is
|
||||
trending?). Instruments are created lazily so this module is safe to
|
||||
import inside a tree-shaken bundle even when the consumer never
|
||||
configures a `MeterProvider`.
|
||||
|
||||
## Features
|
||||
|
||||
- Lazy instrument creation — first `getDriveReactSharingInstruments()`
|
||||
call resolves the meter and caches the counter triple.
|
||||
- `resetDriveReactSharingInstruments()` clears the cache so tests can
|
||||
swap a fresh meter provider per case without leaking instruments.
|
||||
- Counter names follow the `drive.react.sharing.*_total` convention so
|
||||
Prometheus / OTLP backends can aggregate the family cheanly.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { getDriveReactSharingInstruments } from "@suaveplan/drive-react-sharing";
|
||||
|
||||
const { recipientsAddedTotal } = getDriveReactSharingInstruments();
|
||||
recipientsAddedTotal.add(1, { partition: "shared" });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Swap meter providers between integration test cases:
|
||||
|
||||
```ts
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import { resetDriveReactSharingInstruments } from "@suaveplan/drive-react-sharing";
|
||||
|
||||
afterEach(() => {
|
||||
metrics.disable();
|
||||
resetDriveReactSharingInstruments();
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface DriveReactSharingInstruments` — `{ recipientsAddedTotal, recipientsRemovedTotal, permissionsUpdatedTotal }`.
|
||||
- `getDriveReactSharingInstruments(): DriveReactSharingInstruments` — lazy accessor.
|
||||
- `resetDriveReactSharingInstruments(): void` — test-only reset.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The instruments are deliberately Counters (not Histograms) because each
|
||||
event is a single mutation with no associated duration distribution.
|
||||
Permission updates carry a `permission` attribute so dashboards can
|
||||
distinguish `read` / `write` / `admin` trends; the other counters carry
|
||||
no attributes by default so cardinality remains bounded.
|
||||
|
||||
Lazy creation matters because every project consumer that pulls in the
|
||||
React-tier sharing surface should pay zero instrument overhead until it
|
||||
actually mutates a grant. The cache lives at module scope so the first
|
||||
caller materialises the counters and every subsequent caller pays only
|
||||
a single property lookup. Tests must call `resetDriveReactSharingInstruments`
|
||||
between cases when they swap the global `MeterProvider`; otherwise the
|
||||
cached counters retain references to the prior provider's meter and
|
||||
new metric points never surface in the swapped backend.
|
||||
|
||||
The decision to scope updates by `permission` attribute rather than by
|
||||
recipient id keeps cardinality at exactly five values matching the
|
||||
permission union (`read` / `write` / `delete` / `share` / `admin`),
|
||||
which is friendly to Prometheus label cardinality budgets and avoids
|
||||
the runaway-tag pitfall common in user-id-attributed counters.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Canonical sharing types
|
||||
|
||||
## Purpose
|
||||
|
||||
This module is the single source of truth for the React-tier sharing
|
||||
type surface. It re-exports the canonical `SharePermission`,
|
||||
`PartitionType`, `Grant`, `UserId`, and `PartitionId` types directly
|
||||
from `@suaveplan/drive-sharing` — the engine package owns the literal
|
||||
sets — and adds three projection types (`ShareRecipient`, `AccessEntry`,
|
||||
`UseSharingOptions` / `UseSharingResult`) tailored to the UI surface.
|
||||
|
||||
On main, three overlapping permission names existed: `SharePermission`,
|
||||
`AccessPermission`, and `SharingPermission`. suaveplan consolidates these
|
||||
into the engine-owned `SharePermission` and refuses to ship a second
|
||||
alias.
|
||||
|
||||
## Features
|
||||
|
||||
- Single canonical re-export of `SharePermission` covering every
|
||||
permission tier accepted by the engine (`read | write | delete | share | admin`).
|
||||
- `ShareRecipient` UI projection — flattens an engine `Grant` plus
|
||||
primary permission for badge rendering.
|
||||
- `AccessEntry` — read-only row used by `<FileAccessList />`.
|
||||
- `UseSharingOptions` / `UseSharingResult` — wire the hook to live
|
||||
engine instances (`PartitionManager`, `GrantManager`, `TokenManager`)
|
||||
without forcing consumers to import three packages.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
SharePermission,
|
||||
ShareRecipient,
|
||||
} from "@suaveplan/drive-react-sharing";
|
||||
|
||||
const tier: SharePermission = "write";
|
||||
function render(recipients: readonly ShareRecipient[]): void {
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
UseSharingOptions,
|
||||
UseSharingResult,
|
||||
} from "@suaveplan/drive-react-sharing";
|
||||
|
||||
function buildOptions(
|
||||
partitionId: string,
|
||||
grantor: UseSharingOptions["grantorId"],
|
||||
deps: Pick<UseSharingOptions, "partitions" | "grants" | "tokens">
|
||||
): UseSharingOptions {
|
||||
return { partitionId, grantorId: grantor, ...deps };
|
||||
}
|
||||
|
||||
function summarize(result: UseSharingResult): string {
|
||||
return `${result.recipients.length} recipients (${result.loading ? "loading" : "idle"})`;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type SharePermission = "read" | "write" | "delete" | "share" | "admin"`
|
||||
(re-exported from `@suaveplan/drive-sharing`).
|
||||
- `type PartitionType = "personal" | "shared" | "public" | "encrypted"`.
|
||||
- `interface ShareRecipient` — UI projection of a `Grant`.
|
||||
- `interface AccessEntry` — read-only access-list row.
|
||||
- `interface UseSharingOptions` — `{ partitionId, grantorId, partitions, grants, tokens, initialRecipients? }`.
|
||||
- `interface UseSharingResult` — `{ recipients, loading, error, addRecipient, removeRecipient, updatePermission, refresh }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`ShareRecipient.primaryPermission` is the highest tier present in the
|
||||
flat `permissions` array — chosen for badge colour selection. The hook
|
||||
computes this when constructing recipients from the engine, so consumers
|
||||
should treat the field as authoritative for display purposes but still
|
||||
consult the full `permissions` array for entitlement decisions.
|
||||
Reference in New Issue
Block a user