@colixsystems/widget-sdk 0.113.0 → 0.115.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/dist/contract.cjs +36 -1
- package/dist/contract.js +36 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/interaction.js +16 -0
- package/dist/interaction.native.js +10 -0
- package/dist/linter.cjs +15 -9
- package/dist/linter.js +15 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
17
17
|
|
|
18
18
|
| Group | Hook (signature) | Returns | Reads / scope |
|
|
19
19
|
| ----- | ---------------- | ------- | ------------- |
|
|
20
|
-
| **CORE** | `useTheme()` | `{ colors, elevation, spacing, spacingScale, radii, typography, components, widgetStyles }` | `ctx.workspace.theme` — no scope. `elevation` is the shared depth scale (`none / sm / md / lg / xl`) you spread into a style; `colors` includes the accent's quiet tiers (`primarySoft` / `onPrimarySoft` / `primaryStrong`) and `loader`, the spinner colour for your own loading state. `components` is HOST-OWNED (the theme's per-component style tokens); the host has already folded it into your `props.style`, so read `useWidgetStyle()` and ignore this slice. |
|
|
20
|
+
| **CORE** | `useTheme()` | `{ colors, elevation, interaction, spacing, spacingScale, radii, typography, components, widgetStyles }` | `ctx.workspace.theme` — no scope. `elevation` is the shared depth scale (`none / sm / md / lg / xl`) you spread into a style; `colors` includes the accent's quiet tiers (`primarySoft` / `onPrimarySoft` / `primaryStrong`) and `loader`, the spinner colour for your own loading state. `components` is HOST-OWNED (the theme's per-component style tokens); the host has already folded it into your `props.style`, so read `useWidgetStyle()` and ignore this slice. |
|
|
21
21
|
| **CORE** | `useWorkspaceCurrency()` | `{ currency, formatMoney }` | `ctx.workspace.currency` — no scope. The currency this workspace charges its app users in, resolved at RENDER time. Render every price as `formatMoney(minorUnits)` and never write a currency symbol or code into a widget: the owner can change it after the widget ships, and a baked label then contradicts the charge. |
|
|
22
22
|
| **CORE** | `useWidgetStyle()` | `{ [styleField]: value }` | `ctx.props.style` — no scope. The author-set per-widget style values declared in `manifest.styleSchema`; apply each onto whatever element you choose. |
|
|
23
23
|
| **CORE** | `useUser()` | `{ id, email, displayName, roles, groupIds }` | `ctx.user` (host-built context, **camelCase** — not a wire payload; `id` null when anonymous) — no scope |
|
|
@@ -49,7 +49,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
49
49
|
| **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*` |
|
|
50
50
|
| **DATASTORE** | `useDatastoreSubscription(table, handlers, options?)` | `{ status }` — `"connecting" \| "live" \| "reconnecting" \| "fallback"` | `records(table).subscribe` — `datastore.read:<table>`. Live `onCreated` / `onUpdated` / `onDeleted` off the REQ-RT-07 socket; never throws, resolving to `{ status: "fallback" }` so the widget polls instead. A whole-table subscribe is gated on read-EVERY-row, because one envelope reaches every subscriber of the table — so for a table governed by per-record grants pass `options.scope`: `{ kind: "record", record_id }` for one row, or `{ kind: "parent", relation_column, record_id }` for the rows whose RELATION column points at that parent (the column must carry `inheritAcl`, else the subscribe reports `"fallback"`). Re-subscribes on the scope's VALUES, so a fresh object literal each render is fine. |
|
|
51
51
|
| **DATASTORE** | `useRecordPermissions(tableId, recordId)` | `{ permissions, loading, error, grant, revoke, update, refetch }` | `records(table).permissions(record).{ list, grant, update, revoke }` — `acl.write:records` (+ `can_grant` on the record) |
|
|
52
|
-
| **DATASTORE** | `useCanWrite(tableId, options?)` | `{ canWrite, loading, error, refetch }` | `myPermissions(tableId, { recordId? })` — scope `datastore.read:<table>`. A FLOOR, not a full replacement for domain-specific write rules: answers "
|
|
52
|
+
| **DATASTORE** | `useCanWrite(tableId, options?)` | `{ canWrite, loading, error, refetch }` | `myPermissions(tableId, { recordId? })` — scope `datastore.read:<table>`. A FLOOR, not a full replacement for domain-specific write rules: answers "may this caller write", reading the same table-ACL answer the write endpoint enforces — so a table granting Create to Everyone answers `true` for a logged-out visitor, and this hook alone is the right gate for a widget meant to work without signing in. Pass `{ recordId }` for a per-row check. A widget whose own rule is MORE SPECIFIC than the table ACL (e.g. "only the assigned user may edit this row") must still hand-check that in addition. Pair with `useUser()` to also tell "not signed in" apart from "signed in but forbidden" — both resolve `canWrite: false` here. Falsy `tableId`, or a host that hasn't injected `myPermissions` (an older host), collapses to `{ canWrite: false, loading: false, error: null, refetch: async () => undefined }` rather than throwing. |
|
|
53
53
|
| **FILES** (`ctx.assets`) | `useAsset(id)` | `{ url, file, loading, error, refetch }` | `ctx.assets.get` — no scope |
|
|
54
54
|
| **FILES** | `useAssetsByTag(tag, { type? })` | `{ assets, loading, error, refetch }` | `ctx.assets.list` (unwraps `{ data, meta }` to `assets`) — no scope. `type` defaults to `"image"`; pass `"all"` / `"audio"` / `"video"` / `"document"` to widen. Falsy `tag` collapses to `assets: []` without a round-trip. |
|
|
55
55
|
| **DIRECTORY** (`ctx.directory`) | `useDirectory(query?)` | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
|
|
@@ -72,6 +72,16 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
72
72
|
|
|
73
73
|
`v0.112.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
74
74
|
|
|
75
|
+
### What's new in 0.115.0 (contract unchanged)
|
|
76
|
+
|
|
77
|
+
**`write-not-gated-on-user` now accepts a `useCanWrite()` gate — a widget may be opened to logged-out visitors (sc-6593).** The rule (added in 0.89.0, below) flagged any `useDatastoreMutation` write that carried no identity guard, and only a `.id` / `groupIds` / `roles` check counted as one. That encoded "a write needs a signed-in app user" as a platform fact, which it is not: a table whose permissions grant **Create** to *Everyone (anonymous + signed-in)* accepts a write from a logged-out visitor, and `useCanWrite(tableId)` answers `true` for them.
|
|
78
|
+
|
|
79
|
+
So a widget gated on `useCanWrite` alone — the correct shape for a public tally, a guest sign-up sheet, or an open feedback form — used to trip the warning that steers the AI widget agent's repair loop back to identity gating, making the sign-in requirement impossible for an author to remove. It is now recognised as a gate, and its finding label names it first.
|
|
80
|
+
|
|
81
|
+
Nothing else changes: a widget with **no** gate at all is still flagged, `useUser().id` read purely as a VALUE still does not satisfy the rule, and the severity is still `warning` (never publish-blocking). Identity gating remains the right default for almost every write — this only stops the linter from arguing against the one case where it isn't.
|
|
82
|
+
|
|
83
|
+
When you take the `useCanWrite`-only route, omit the USER column for a guest (`if (user.id) payload[byField] = user.id;`) — an anonymous row records no author, so per-person limits and "my entries" views cannot work for one.
|
|
84
|
+
|
|
75
85
|
### What's new in 0.113.0 (contract 1.86.0)
|
|
76
86
|
|
|
77
87
|
**New `useCamera()` hook — take a photo or pick one from the device library.** A new CORE hook reading a new `camera` capability on the existing `ctx.device` slice. Returns `{ asset, loading, error, supported, capture, pick, reset }`. Capture is **imperative** — call `capture()` or `pick()` from a user gesture (a `Pressable.onPress`); the browser and the mobile OS gate the permission prompt on a gesture, so it NEVER opens on mount. `options` (`{ allowsEditing, quality }`) pass through to the host. It needs **no manifest scope** and **no `requestedScopes` entry**.
|
|
@@ -1094,15 +1104,16 @@ A widget that works but looks unfinished is only half done. `useTheme()` is the
|
|
|
1094
1104
|
- **Contain and elevate.** Wrap a logical unit in a surface: `colors.surface` + padding + `radii.lg` + `...theme.elevation.sm`. Give it the elevation **or** a `colors.border` hairline, not both — and prefer the elevation, because a hairline-only card reads as a wireframe. `theme.elevation` is a token table you spread into a style (`...theme.elevation.md`), covering `none / sm / md / lg / xl`; never hand-write `shadowOpacity` / `shadowRadius` / `boxShadow`. Use the status roles (`danger / success / warning / info`) for state.
|
|
1095
1105
|
- **Tint the supporting cast.** `colors.primarySoft` is a tint of the workspace accent over the surface and `colors.onPrimarySoft` is guaranteed readable on it (WCAG AA, on light and dark themes alike). Use the pair for chips, secondary buttons, progress tracks, icon badges and selected rows. One saturated accent moment surrounded by several pale echoes of the same hue is what reads as designed — a row of grey-outlined buttons reads as a form. Never hand-mix a tint with `rgba(...)` or a translucent overlay.
|
|
1096
1106
|
- **Spend one gradient.** `<Gradient colors={[theme.colors.primary, theme.colors.primaryStrong]} angle={160} style={…}>` is a `View` that paints a gradient behind its children, so it replaces the `View` you'd otherwise give a flat `backgroundColor`. `angle` is CSS degrees (0 = to top, 90 = to right, default 180); text on it uses `colors.onPrimary`. Exactly **one** per widget — on the focal element — and never behind body text. Both hosts render it identically (web paints CSS, native uses `expo-linear-gradient`), so there is no per-platform branching to write; don't import `expo-linear-gradient` yourself and don't write a `backgroundImage` string.
|
|
1107
|
+
- **Answer the touch.** Every tappable card, row and list entry lifts while the pointer is over it (web) or it is pressed (touch). One declaration does both: give the Pressable a style FUNCTION and spread `pressableLift` — `<Pressable onPress={open} style={(state) => [styles.card, ...pressableLift(state)]}>`. The lift is a -2px nudge plus one elevation step from `theme.interaction`, with the web transition built in. Never hand-write hover logic or your own pressed shadows, and never fake feedback with `opacity` — a dimmed surface reads as disabling itself.
|
|
1097
1108
|
- **Size to your container — measure it, don't stretch into it.** The same widget sits in a full-width desktop section (~1400px), a half-width grid cell (~700px) and a phone (~360px), so layout built only from `flex: 1` stretches to fill whatever it is handed — a month calendar ends up with 200px day cells and swallows the page. Measure your own width with `onLayout={(e) => setWidth(e.nativeEvent.layout.width)}` on the root `View` (a React Native primitive, so it behaves identically on both hosts), render nothing size-dependent while `width === 0`, and compute every threshold from the measured value: a widget gets no declared breakpoint prop, but it can always measure. **Cap a repeating cell** rather than giving a grid `flex: 1` — `const cell = Math.max(32, Math.min(Math.floor((usable - gap * (columns - 1)) / columns), 64));`, with a calendar day cell topping out at 56–72px on `aspectRatio: 1`, and the grid given its exact computed width plus `alignSelf: 'center'` when the cap leaves slack. **Split two co-equal surfaces above ~720px measured width** (`flexDirection: width >= 720 ? 'row' : 'column'`, each half `{ flex: 1, minWidth: 0 }`) — a picker beside the form it feeds on a wide canvas, stacked in reading order below it. Never hardcode a width, never put `flex: 1` / `height: '100%'` on a content widget's root, and don't read the screen with `Dimensions` — the screen is not the widget.
|
|
1098
1109
|
- **Compose forms — pair fields into rows, don't stack one per row.** Put short, related fields side by side (first + last name, city + postal code, expiry + CVC): a row of `{ flexDirection: 'row', flexWrap: 'wrap', gap: theme.spacing.md }` with each field cell `{ flexGrow: 1, flexBasis: 160 }` splits the width on a wide card and wraps to stacked on a narrow phone — the right recipe for field pairs because it needs no measurement (a fixed-width column overflows a phone; when a layout needs a real column count instead of wrapping, measure your width as above). Keep wide fields (email, address, notes) full-width, cap it at two–three per row, group a long form into labelled sections, and label every input above it (not placeholder-only). Give a `multiline` field BOTH a floor and a ceiling (`{ minHeight: 150, maxHeight: 260 }`) so a long value scrolls inside the box instead of growing past its card, and keep the Save / Cancel row in normal flow below the fields, never positioned over them.
|
|
1099
|
-
- **Respond to touch.** Give every `Pressable`
|
|
1110
|
+
- **Respond to touch.** Give every `Pressable` the lift via the function-style `style={(state) => [base, ...pressableLift(state)]}` — see "Answer the touch" above. Never dim with `opacity`, which reads as the surface disabling itself.
|
|
1100
1111
|
- **Drag and drop — show what is being dragged.** A drag where the item stays put reads as broken. Three things change the moment a drag starts: the **drag proxy** (the item lifts and follows the finger — `...theme.elevation.lg`, `{ scale: 1.03 }`, `opacity: 0.9`; for a tall or full-width item drag a compact `primarySoft` pill with its icon + one line of label instead), the **source placeholder** (the vacated slot keeps its height as a quiet `colors.surfaceMuted` block so the list doesn't collapse), and the **drop target** (one slot at a time highlighted with `primarySoft` or a 2px `colors.primary` border). Always animate the release — settle into the new slot, or `Animated.spring(pan, { toValue: { x: 0, y: 0 }, useNativeDriver: false })` back to the origin on cancel. Build it with `Animated` + `PanResponder` from `react-native` (the only mechanism that behaves identically on both hosts) — never HTML5 drag events (`draggable` / `onDragStart` / `dataTransfer` are web-only, and `document` / `window` are banned) — and start the drag from a `GripVertical` grip handle whenever the row is also tappable or sits in a `ScrollView`.
|
|
1101
1112
|
- **Use icons for clarity.** Pair a `lucide-react-native` icon with its label at a consistent size, coloured from the theme. The label never repeats the icon as a character — with a `Plus` icon the button says "Add item", never "+ Add item" (that renders a doubled plus).
|
|
1102
1113
|
- **Use imagery deliberately.** Render pictures with the `Image` primitive (`source` takes a URL or `{ uri }`); resolve workspace assets via `useAsset()`. Give every image a sized, `radii`-clipped container so it never renders as a raw rectangle, and never hardcode a credentialed image URL — expose an `image`-type property instead. The frame is your decision, never the picture's: size it for the role (a 40–56 square avatar, a 72–96 square row thumbnail, a `16 / 9` card cover, a 160–240 tall band) and let `resizeMode="cover"` crop the photo into it — photos arrive at every size and ratio, so one left to its own proportions breaks the layout. Keep `contain` for art whose whole subject must stay visible (a logo, a diagram), inside a fixed frame.
|
|
1103
1114
|
- **Design the empty, loading, and error states.** A blank box on a fresh install reads as broken — show a short helper line when a list is empty, a calm loading line, and a single human sentence in `colors.danger` on error.
|
|
1104
1115
|
|
|
1105
|
-
**Honest ceilings:** the styling surface is React Native style objects, not full CSS. Gradients come from the `<Gradient>` primitive (not a CSS `linear-gradient` string), depth comes from `theme.elevation` (not arbitrary `box-shadow` stacks), and there are no custom CSS keyframe animations or `transition` strings, no `filter` / `backdrop-filter` / `clip-path` / `mask` / blend modes, and no opacity-faked tints (that's what `primarySoft` is for). Aim for clean, confident, professional polish within those bounds — lifted surfaces, generous corners, one accent moment.
|
|
1116
|
+
**Honest ceilings:** the styling surface is React Native style objects, not full CSS. Gradients come from the `<Gradient>` primitive (not a CSS `linear-gradient` string), depth comes from `theme.elevation` (not arbitrary `box-shadow` stacks), and there are no custom CSS keyframe animations or `transition` strings (the hover/press lift comes built into `pressableLift` — never write your own), no `filter` / `backdrop-filter` / `clip-path` / `mask` / blend modes, and no opacity-faked tints (that's what `primarySoft` is for). Aim for clean, confident, professional polish within those bounds — lifted surfaces, generous corners, one accent moment.
|
|
1106
1117
|
|
|
1107
1118
|
## Managing app users from a widget
|
|
1108
1119
|
|
package/dist/contract.cjs
CHANGED
|
@@ -45,6 +45,24 @@ const ELEVATION = Object.freeze({
|
|
|
45
45
|
}),
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
// sc-6531 (REQ-AI-AGENT-DESIGN-LIFT): the ONE cross-platform interaction
|
|
49
|
+
// vocabulary. A tappable surface answers the pointer over it (web) and the
|
|
50
|
+
// press on it (touch) by LIFTING. `raised` is one elevation step up plus a
|
|
51
|
+
// nudge — the scale above stays the only shadow vocabulary — and is what a
|
|
52
|
+
// widget's own surface wears via `pressableLift`; `translateY` alone is the
|
|
53
|
+
// surface-agnostic nudge a tappable layout REGION gets on both hosts.
|
|
54
|
+
const INTERACTION = Object.freeze({
|
|
55
|
+
lift: Object.freeze({
|
|
56
|
+
translateY: -2,
|
|
57
|
+
raised: Object.freeze({
|
|
58
|
+
transform: Object.freeze([Object.freeze({ translateY: -2 })]),
|
|
59
|
+
...ELEVATION.lg,
|
|
60
|
+
}),
|
|
61
|
+
// Web-only smoothing; a touch state change is instant.
|
|
62
|
+
transitionMs: 150,
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
|
|
48
66
|
const DEFAULT_THEME_TOKENS = Object.freeze({
|
|
49
67
|
colors: Object.freeze({
|
|
50
68
|
primary: "#3b82f6",
|
|
@@ -76,6 +94,7 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
|
|
|
76
94
|
info: "#0284c7",
|
|
77
95
|
}),
|
|
78
96
|
elevation: ELEVATION,
|
|
97
|
+
interaction: INTERACTION,
|
|
79
98
|
spacing: Object.freeze({ xs: 4, sm: 8, md: 16, lg: 24, xl: 32 }),
|
|
80
99
|
// REQ-THEME-LOOK: multiplies every layout spacing value at render. 1 is
|
|
81
100
|
// unchanged, so a theme that never sets it renders exactly as before.
|
|
@@ -1740,6 +1759,17 @@ const PRIMITIVES = [
|
|
|
1740
1759
|
rnComponent: "expo-linear-gradient",
|
|
1741
1760
|
docsUrl: "https://docs.expo.dev/versions/latest/sdk/linear-gradient/",
|
|
1742
1761
|
},
|
|
1762
|
+
// sc-6531 (REQ-AI-AGENT-DESIGN-LIFT) — the hover/press lift affordance. A
|
|
1763
|
+
// FUNCTION primitive, not a component: it resolves the interaction tokens per
|
|
1764
|
+
// input, so one declaration answers the pointer on web and the press on
|
|
1765
|
+
// native.
|
|
1766
|
+
{
|
|
1767
|
+
name: "pressableLift",
|
|
1768
|
+
description:
|
|
1769
|
+
"Interaction feedback for a tappable surface. Call it inside a Pressable's style FUNCTION and spread the result: `<Pressable style={(state) => [styles.card, ...pressableLift(state)]}>`. While the pointer hovers the surface (web) or it is pressed (touch), the surface lifts: a -2px nudge plus one elevation step, from `themeTokens.interaction`, with the web transition built in. Give it to EVERY tappable card, row and list entry; never hand-write hover logic, `transition` strings, or your own pressed shadows.",
|
|
1770
|
+
rnComponent: null,
|
|
1771
|
+
docsUrl: null,
|
|
1772
|
+
},
|
|
1743
1773
|
];
|
|
1744
1774
|
|
|
1745
1775
|
const CATEGORIES = [
|
|
@@ -3433,7 +3463,12 @@ const CONTRACT = deepFreeze({
|
|
|
3433
3463
|
// docs/design/req-widget-sdk-cross-platform-primitives.md, which was
|
|
3434
3464
|
// about vetting a full camera surface (permissions, multi-step UX, frame
|
|
3435
3465
|
// processing) as a widget import — none of which this adds.
|
|
3436
|
-
|
|
3466
|
+
// 1.87.0: additive (sc-6531) — `themeTokens.interaction`: the interaction-
|
|
3467
|
+
// state vocabulary (`lift.raised`, `lift.translateY`, `lift.transitionMs`),
|
|
3468
|
+
// plus the `pressableLift` primitive that resolves it per input — hover on
|
|
3469
|
+
// web, press on native. Tappable layout containers and widget surfaces lift
|
|
3470
|
+
// from this ONE table.
|
|
3471
|
+
version: "1.87.0",
|
|
3437
3472
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3438
3473
|
hooks: HOOKS,
|
|
3439
3474
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -45,6 +45,24 @@ const ELEVATION = Object.freeze({
|
|
|
45
45
|
}),
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
// sc-6531 (REQ-AI-AGENT-DESIGN-LIFT): the ONE cross-platform interaction
|
|
49
|
+
// vocabulary. A tappable surface answers the pointer over it (web) and the
|
|
50
|
+
// press on it (touch) by LIFTING. `raised` is one elevation step up plus a
|
|
51
|
+
// nudge — the scale above stays the only shadow vocabulary — and is what a
|
|
52
|
+
// widget's own surface wears via `pressableLift`; `translateY` alone is the
|
|
53
|
+
// surface-agnostic nudge a tappable layout REGION gets on both hosts.
|
|
54
|
+
const INTERACTION = Object.freeze({
|
|
55
|
+
lift: Object.freeze({
|
|
56
|
+
translateY: -2,
|
|
57
|
+
raised: Object.freeze({
|
|
58
|
+
transform: Object.freeze([Object.freeze({ translateY: -2 })]),
|
|
59
|
+
...ELEVATION.lg,
|
|
60
|
+
}),
|
|
61
|
+
// Web-only smoothing; a touch state change is instant.
|
|
62
|
+
transitionMs: 150,
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
|
|
48
66
|
const DEFAULT_THEME_TOKENS = Object.freeze({
|
|
49
67
|
colors: Object.freeze({
|
|
50
68
|
primary: "#3b82f6",
|
|
@@ -76,6 +94,7 @@ const DEFAULT_THEME_TOKENS = Object.freeze({
|
|
|
76
94
|
info: "#0284c7",
|
|
77
95
|
}),
|
|
78
96
|
elevation: ELEVATION,
|
|
97
|
+
interaction: INTERACTION,
|
|
79
98
|
spacing: Object.freeze({ xs: 4, sm: 8, md: 16, lg: 24, xl: 32 }),
|
|
80
99
|
// REQ-THEME-LOOK: multiplies every layout spacing value at render. 1 is
|
|
81
100
|
// unchanged, so a theme that never sets it renders exactly as before.
|
|
@@ -1740,6 +1759,17 @@ const PRIMITIVES = [
|
|
|
1740
1759
|
rnComponent: "expo-linear-gradient",
|
|
1741
1760
|
docsUrl: "https://docs.expo.dev/versions/latest/sdk/linear-gradient/",
|
|
1742
1761
|
},
|
|
1762
|
+
// sc-6531 (REQ-AI-AGENT-DESIGN-LIFT) — the hover/press lift affordance. A
|
|
1763
|
+
// FUNCTION primitive, not a component: it resolves the interaction tokens per
|
|
1764
|
+
// input, so one declaration answers the pointer on web and the press on
|
|
1765
|
+
// native.
|
|
1766
|
+
{
|
|
1767
|
+
name: "pressableLift",
|
|
1768
|
+
description:
|
|
1769
|
+
"Interaction feedback for a tappable surface. Call it inside a Pressable's style FUNCTION and spread the result: `<Pressable style={(state) => [styles.card, ...pressableLift(state)]}>`. While the pointer hovers the surface (web) or it is pressed (touch), the surface lifts: a -2px nudge plus one elevation step, from `themeTokens.interaction`, with the web transition built in. Give it to EVERY tappable card, row and list entry; never hand-write hover logic, `transition` strings, or your own pressed shadows.",
|
|
1770
|
+
rnComponent: null,
|
|
1771
|
+
docsUrl: null,
|
|
1772
|
+
},
|
|
1743
1773
|
];
|
|
1744
1774
|
|
|
1745
1775
|
const CATEGORIES = [
|
|
@@ -3433,7 +3463,12 @@ const CONTRACT = deepFreeze({
|
|
|
3433
3463
|
// docs/design/req-widget-sdk-cross-platform-primitives.md, which was
|
|
3434
3464
|
// about vetting a full camera surface (permissions, multi-step UX, frame
|
|
3435
3465
|
// processing) as a widget import — none of which this adds.
|
|
3436
|
-
|
|
3466
|
+
// 1.87.0: additive (sc-6531) — `themeTokens.interaction`: the interaction-
|
|
3467
|
+
// state vocabulary (`lift.raised`, `lift.translateY`, `lift.transitionMs`),
|
|
3468
|
+
// plus the `pressableLift` primitive that resolves it per input — hover on
|
|
3469
|
+
// web, press on native. Tappable layout containers and widget surfaces lift
|
|
3470
|
+
// from this ONE table.
|
|
3471
|
+
version: "1.87.0",
|
|
3437
3472
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3438
3473
|
hooks: HOOKS,
|
|
3439
3474
|
primitives: PRIMITIVES,
|
package/dist/index.d.ts
CHANGED
|
@@ -2312,3 +2312,15 @@ export const CONTRACT: AiWidgetContract;
|
|
|
2312
2312
|
|
|
2313
2313
|
export function isHookAllowed(name: string): boolean;
|
|
2314
2314
|
export function requiredContextKeys(): string[];
|
|
2315
|
+
|
|
2316
|
+
/**
|
|
2317
|
+
* sc-6531 (REQ-AI-AGENT-DESIGN-LIFT) — interaction feedback for a tappable
|
|
2318
|
+
* surface. Call inside a Pressable's style function and spread the result:
|
|
2319
|
+
* `<Pressable style={(state) => [styles.card, ...pressableLift(state)]}>`.
|
|
2320
|
+
* Web raises the surface while hovered or pressed (with the transition built
|
|
2321
|
+
* in); native raises it while pressed.
|
|
2322
|
+
*/
|
|
2323
|
+
export function pressableLift(state?: {
|
|
2324
|
+
hovered?: boolean;
|
|
2325
|
+
pressed?: boolean;
|
|
2326
|
+
}): Array<Record<string, unknown> | null>;
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// sc-6531 (REQ-AI-AGENT-DESIGN-LIFT) — the web half of the lift affordance.
|
|
2
|
+
// react-native-web's Pressable hands its style function `{ hovered, pressed }`;
|
|
3
|
+
// either one raises the surface, and the smoothing rides the RESTING style so
|
|
4
|
+
// the lift animates in both directions. The touch half is interaction.native.js
|
|
5
|
+
// — no hover exists there, and a pressed state change is instant.
|
|
6
|
+
import { CONTRACT } from "./contract.js";
|
|
7
|
+
|
|
8
|
+
const LIFT = CONTRACT.themeTokens.interaction.lift;
|
|
9
|
+
const SMOOTHING = Object.freeze({
|
|
10
|
+
transitionProperty: "transform, box-shadow",
|
|
11
|
+
transitionDuration: `${LIFT.transitionMs}ms`,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export function pressableLift(state = {}) {
|
|
15
|
+
return [SMOOTHING, state.hovered || state.pressed ? LIFT.raised : null];
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// sc-6531 (REQ-AI-AGENT-DESIGN-LIFT) — the touch half of the lift affordance:
|
|
2
|
+
// there is no hover, the press state raises the surface, and the change is
|
|
3
|
+
// instant (RN styles carry no transitions). The pointer half is interaction.js.
|
|
4
|
+
import { CONTRACT } from "./contract.js";
|
|
5
|
+
|
|
6
|
+
const LIFT = CONTRACT.themeTokens.interaction.lift;
|
|
7
|
+
|
|
8
|
+
export function pressableLift(state = {}) {
|
|
9
|
+
return [state.pressed ? LIFT.raised : null];
|
|
10
|
+
}
|
package/dist/linter.cjs
CHANGED
|
@@ -928,12 +928,17 @@ const CURRENCY_LABEL_RES = [
|
|
|
928
928
|
];
|
|
929
929
|
|
|
930
930
|
// sc-4985 — soft warning: a widget that writes must decide what a signed-OUT
|
|
931
|
-
// visitor sees. A
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
// a
|
|
935
|
-
//
|
|
936
|
-
//
|
|
931
|
+
// visitor sees. A visitor handed a live "Save" / "Book" / "Delete" button the
|
|
932
|
+
// table will refuse can only tap it and fail — the failure the gate exists to
|
|
933
|
+
// spare them. Satisfied by any identity guard: a negated or compared `.id`, or
|
|
934
|
+
// a `groupIds` / `roles` check. Reading `useUser().id` purely as a VALUE (the
|
|
935
|
+
// USER-column write pattern) is NOT a guard, which is why an operator follows.
|
|
936
|
+
//
|
|
937
|
+
// sc-6593 — `useCanWrite(tableId)` satisfies it too, and answers better: it
|
|
938
|
+
// reads the ACL the write endpoint enforces, so it goes live for whoever may
|
|
939
|
+
// write. Since a table granting Create to EVERYONE accepts an anonymous write
|
|
940
|
+
// (sc-5229), demanding identity here would flag the only correct way to build
|
|
941
|
+
// the logged-out-friendly widget an author explicitly asked for.
|
|
937
942
|
//
|
|
938
943
|
// Conservative on purpose: an unrelated `.id` comparison elsewhere in the
|
|
939
944
|
// source silences the rule. A warning that occasionally stays quiet is far
|
|
@@ -952,6 +957,7 @@ function _writeGatedOnUserRules(source) {
|
|
|
952
957
|
const code = _stripNonCode(source);
|
|
953
958
|
const call = /\buseDatastoreMutation\s*\(/.exec(code);
|
|
954
959
|
if (!call) return [];
|
|
960
|
+
if (/\buseCanWrite\s*\(/.test(code)) return [];
|
|
955
961
|
if (_IDENTITY_GUARD_RES.some((re) => re.test(code))) return [];
|
|
956
962
|
const line = code.slice(0, call.index).split(/\r?\n/).length;
|
|
957
963
|
return [
|
|
@@ -961,9 +967,9 @@ function _writeGatedOnUserRules(source) {
|
|
|
961
967
|
// Kept under ~210 chars: a finding is truncated at 300 downstream, and
|
|
962
968
|
// the fix instruction is the half worth keeping.
|
|
963
969
|
label:
|
|
964
|
-
`writes with useDatastoreMutation() but never checks who
|
|
965
|
-
|
|
966
|
-
`
|
|
970
|
+
`writes with useDatastoreMutation() but never checks who may write ` +
|
|
971
|
+
`- gate on useCanWrite(tableId), or useUser() when the action needs ` +
|
|
972
|
+
`a signed-in user, and render the control inactive, never live-but-doomed.`,
|
|
967
973
|
line,
|
|
968
974
|
snippet: (source.split(/\r?\n/)[line - 1] || "").trim().slice(0, 200),
|
|
969
975
|
},
|
package/dist/linter.js
CHANGED
|
@@ -1077,12 +1077,17 @@ const CURRENCY_LABEL_RES = [
|
|
|
1077
1077
|
];
|
|
1078
1078
|
|
|
1079
1079
|
// sc-4985 — soft warning: a widget that writes must decide what a signed-OUT
|
|
1080
|
-
// visitor sees. A
|
|
1081
|
-
//
|
|
1082
|
-
//
|
|
1083
|
-
// a
|
|
1084
|
-
//
|
|
1085
|
-
//
|
|
1080
|
+
// visitor sees. A visitor handed a live "Save" / "Book" / "Delete" button the
|
|
1081
|
+
// table will refuse can only tap it and fail — the failure the gate exists to
|
|
1082
|
+
// spare them. Satisfied by any identity guard: a negated or compared `.id`, or
|
|
1083
|
+
// a `groupIds` / `roles` check. Reading `useUser().id` purely as a VALUE (the
|
|
1084
|
+
// USER-column write pattern) is NOT a guard, which is why an operator follows.
|
|
1085
|
+
//
|
|
1086
|
+
// sc-6593 — `useCanWrite(tableId)` satisfies it too, and answers better: it
|
|
1087
|
+
// reads the ACL the write endpoint enforces, so it goes live for whoever may
|
|
1088
|
+
// write. Since a table granting Create to EVERYONE accepts an anonymous write
|
|
1089
|
+
// (sc-5229), demanding identity here would flag the only correct way to build
|
|
1090
|
+
// the logged-out-friendly widget an author explicitly asked for.
|
|
1086
1091
|
//
|
|
1087
1092
|
// Conservative on purpose: an unrelated `.id` comparison elsewhere in the
|
|
1088
1093
|
// source silences the rule. A warning that occasionally stays quiet is far
|
|
@@ -1101,6 +1106,7 @@ function _writeGatedOnUserRules(source) {
|
|
|
1101
1106
|
const code = _stripNonCode(source);
|
|
1102
1107
|
const call = /\buseDatastoreMutation\s*\(/.exec(code);
|
|
1103
1108
|
if (!call) return [];
|
|
1109
|
+
if (/\buseCanWrite\s*\(/.test(code)) return [];
|
|
1104
1110
|
if (_IDENTITY_GUARD_RES.some((re) => re.test(code))) return [];
|
|
1105
1111
|
const line = code.slice(0, call.index).split(/\r?\n/).length;
|
|
1106
1112
|
return [
|
|
@@ -1110,9 +1116,9 @@ function _writeGatedOnUserRules(source) {
|
|
|
1110
1116
|
// Kept under ~210 chars: a finding is truncated at 300 downstream, and
|
|
1111
1117
|
// the fix instruction is the half worth keeping.
|
|
1112
1118
|
label:
|
|
1113
|
-
`writes with useDatastoreMutation() but never checks who
|
|
1114
|
-
|
|
1115
|
-
`
|
|
1119
|
+
`writes with useDatastoreMutation() but never checks who may write ` +
|
|
1120
|
+
`- gate on useCanWrite(tableId), or useUser() when the action needs ` +
|
|
1121
|
+
`a signed-in user, and render the control inactive, never live-but-doomed.`,
|
|
1116
1122
|
line,
|
|
1117
1123
|
snippet: (source.split(/\r?\n/)[line - 1] || "").trim().slice(0, 200),
|
|
1118
1124
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.115.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "node scripts/build.js",
|
|
51
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/vetted-imports-audit.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-page-url.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-hardcoded-design.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/corner-radius.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/toast-host.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js src/__tests__/hooks-camera.test.js src/__tests__/hooks-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js"
|
|
51
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/vetted-imports-audit.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-page-url.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-hardcoded-design.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/corner-radius.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/interaction-lift.test.js src/__tests__/toast-host.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js src/__tests__/hooks-camera.test.js src/__tests__/hooks-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=18"
|