jfs-components 0.1.30 → 0.1.33
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/CHANGELOG.md +29 -0
- package/D2C.md +118 -0
- package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
- package/lib/commonjs/components/CompareTable/CompareTable.js +140 -84
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +28 -5
- package/lib/commonjs/components/CounterBadge/CounterBadge.js +78 -0
- package/lib/commonjs/components/FavoriteToggle/FavoriteToggle.js +180 -0
- package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +287 -0
- package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +219 -0
- package/lib/commonjs/components/index.js +35 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +40095 -1
- package/lib/commonjs/design-tokens/figma-modes.generated.js +3 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/CategoryCard/CategoryCard.js +258 -0
- package/lib/module/components/CompareTable/CompareTable.js +140 -84
- package/lib/module/components/ContentSheet/ContentSheet.js +29 -6
- package/lib/module/components/CounterBadge/CounterBadge.js +73 -0
- package/lib/module/components/FavoriteToggle/FavoriteToggle.js +174 -0
- package/lib/module/components/HelloJioInput/HelloJioInput.js +281 -0
- package/lib/module/components/ValueBackMetric/ValueBackMetric.js +214 -0
- package/lib/module/components/index.js +6 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +40095 -1
- package/lib/module/design-tokens/figma-modes.generated.js +3 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +72 -0
- package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +16 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +7 -1
- package/lib/typescript/src/components/CounterBadge/CounterBadge.d.ts +19 -0
- package/lib/typescript/src/components/FavoriteToggle/FavoriteToggle.d.ts +66 -0
- package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +111 -0
- package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +86 -0
- package/lib/typescript/src/components/index.d.ts +5 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +7 -1
- package/scripts/extract-figma-modes.js +359 -0
- package/src/components/CategoryCard/CategoryCard.tsx +404 -0
- package/src/components/CompareTable/CompareTable.tsx +201 -101
- package/src/components/ContentSheet/ContentSheet.tsx +40 -11
- package/src/components/CounterBadge/CounterBadge.tsx +95 -0
- package/src/components/FavoriteToggle/FavoriteToggle.tsx +243 -0
- package/src/components/HelloJioInput/HelloJioInput.tsx +513 -0
- package/src/components/ValueBackMetric/ValueBackMetric.tsx +298 -0
- package/src/components/index.ts +5 -0
- package/src/design-tokens/Coin Variables-variables-full.json +40095 -1
- package/src/design-tokens/figma-modes.generated.ts +3 -0
- package/src/icons/registry.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [0.1.33] - 2026-07-20
|
|
8
|
+
|
|
9
|
+
- **Use [`D2C.md`](./D2C.md) for Design-to-Code.** This is the playbook we want agents and consumers to follow when implementing a Figma screen with `jfs-components`. It ships in the npm package (see `package.json` `files`) so it is available at `node_modules/jfs-components/D2C.md` after install. The workflow is mandatory end-to-end: Figma MCP (`get_design_context` + `get_screenshot`) → mode extraction via `npx jfs-extract-modes` (REST API; Dev seat / file-read token is enough) → build with exact modes → visual QA against the Figma screenshot. Point Cursor rules / agent instructions at this file rather than inventing a parallel D2C process.
|
|
10
|
+
- Packages `D2C.md` alongside the `jfs-extract-modes` CLI so the guide and the tool travel together in every release.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## [0.1.32] - 2026-07-20
|
|
15
|
+
|
|
16
|
+
- Added `jfs-extract-modes` — a CLI shipped with the npm package (`bin` + `scripts/extract-figma-modes.js`) that extracts explicit Figma variable modes per layer via the **Figma REST API** only (one `GET /v1/files/:key` call). No Plugin API, so it works with a **Dev seat** (or any token with file read access) — you do not need an edit / full design seat. After installing `jfs-components`, run `npx jfs-extract-modes <figma-url>` (or `npm run extract-modes` in this repo) with `FIGMA_ACCESS_TOKEN` set. Optional `--add-keys` patches collection keys into the packaged variables JSON for library maintainers.
|
|
17
|
+
- Packaged `Coin Variables-variables-full.json` updated with collection keys so mode resolution is collection-key exact for consumers.
|
|
18
|
+
- `ValueBackMetric` — new optional `onPress` / `disabled` props make the whole card pressable (`Pressable` + `accessibilityRole="button"`), independent of the bottom link’s `onLinkPress`.
|
|
19
|
+
- Regenerated `figma-modes.generated.ts` (adds `HelloJioInput` modes) and icon registry timestamp.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## [0.1.31] - 2026-07-15
|
|
24
|
+
|
|
25
|
+
- Added `CategoryCard` — compact category rail tile (`categoryCard/*` tokens) with image (or `imageSlot`), optional overlapping `Badge`, fixed 60dp width (overridable via `width`), and `active` / `disabled` press states. Label truncation mirrors `Text` via `disableTruncation` / `singleLine`.
|
|
26
|
+
- Added `HelloJioInput` — pill-shaped HelloJio prompt field (`helloJioInput/*` tokens) with leading brand icon, single-line input, and send `IconButton`. Focus drives Idle → Active; `jioPlus` enables the frosted IdleJioPlus glass surface. Controlled/uncontrolled text with `onSubmit` from the send button and keyboard return key; `leading` / `trailing` slots for customization.
|
|
27
|
+
- Added `CounterBadge` — small token-driven value pill (`counterBadge/*` tokens).
|
|
28
|
+
- Added `FavoriteToggle` — glass circular favorite control (`favoriteToggle/*` tokens) with controlled `isActive` / uncontrolled `defaultActive`, `onChange`, and skeleton loading support.
|
|
29
|
+
- Added `ValueBackMetric` — compact metric card (JioPoints-style) with header icon + title, value, caption, and optional bottom `Link`; colours/gaps/padding from `metricdata/*` tokens.
|
|
30
|
+
- `CompareTable` — new `stickyHeaders` and `stickyHeaderLabel` props (default `true`) for sticky section headers in fixed-width scroll mode; horizontal scroll sync rewritten to track the actively dragged `ScrollView` and eliminate jitter from programmatic `scrollTo` echoes.
|
|
31
|
+
- `ContentSheet` — new `maxHeightPercent` prop (default `0.7`) caps sheet height as a fraction of the window and scrolls overflow content.
|
|
32
|
+
- Stories and MDX documentation updated for all new components and props.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
7
36
|
## [0.1.30] - 2026-07-10
|
|
8
37
|
|
|
9
38
|
- `MoneyValue` — new `editable` prop (inline edit mode): tapping the value shows a numeric `TextInput` that accepts only digits + one decimal point; edit commits on blur/submit via new `onValueChange` callback; keyboard avoidance lifts the component above the keyboard using `Keyboard.addListener` + `measureInWindow` (same pattern as `ActionFooter`).
|
package/D2C.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# D2C — Design to Code (jfs-components)
|
|
2
|
+
|
|
3
|
+
You are implementing a screen from a Figma design using the `jfs-components`
|
|
4
|
+
library. The user has pasted a Figma link. Follow this workflow exactly, in
|
|
5
|
+
order. Do not skip steps and do not improvise substitutes for them.
|
|
6
|
+
|
|
7
|
+
## Step 1 — Pull the design through the Figma MCP
|
|
8
|
+
|
|
9
|
+
Extract `fileKey` and `nodeId` from the pasted URL
|
|
10
|
+
(`https://www.figma.com/design/<fileKey>/...?node-id=<a>-<b>` → nodeId is
|
|
11
|
+
`<a>:<b>`).
|
|
12
|
+
|
|
13
|
+
1. Call `get_design_context` for the node. This gives you the component
|
|
14
|
+
structure, layout, props, variants, text content, and Code Connect hints.
|
|
15
|
+
2. Call `get_screenshot` for the same node and keep the image. This is the
|
|
16
|
+
visual ground truth you will QA against in Step 4.
|
|
17
|
+
|
|
18
|
+
The Figma MCP gives you almost everything — except one thing: the **variable
|
|
19
|
+
modes explicitly set on each layer**. Those are invisible to the MCP and they
|
|
20
|
+
change how components render (size, emphasis, appearance, color mode, etc.).
|
|
21
|
+
That is what Step 2 is for.
|
|
22
|
+
|
|
23
|
+
## Step 2 — Extract the modes (mandatory, never skip)
|
|
24
|
+
|
|
25
|
+
Run the mode extractor that ships with this package. It needs a Figma personal
|
|
26
|
+
access token with plain file-read access (a view-only / Dev seat token works;
|
|
27
|
+
no Enterprise scopes, no edit access):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export FIGMA_ACCESS_TOKEN=figd_...
|
|
31
|
+
npx jfs-extract-modes "<the pasted Figma URL>" --out modes.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If `npx` cannot find the bin (e.g. unusual monorepo setup), call the script by
|
|
35
|
+
its path inside the installed package:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node node_modules/jfs-components/scripts/extract-figma-modes.js "<figma-url>" --out modes.json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The output is a flat list — one entry per layer that has explicit modes:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"modes": [
|
|
46
|
+
{ "layer": "Icon Button", "nodeId": "1:10638",
|
|
47
|
+
"path": "CC / Unsub > AppBar > Slot 2 > Icon Button",
|
|
48
|
+
"modes": { "Button / Size": "S", "Emphasis": "Low", "AppearanceBrand": "Neutral" } }
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Rules for using it:
|
|
54
|
+
|
|
55
|
+
- Children inherit ancestor modes unless they set their own. To get the
|
|
56
|
+
effective modes of any layer, merge every entry whose `path` is a prefix of
|
|
57
|
+
that layer's path, top-down.
|
|
58
|
+
- Correlate entries with the Figma MCP structure by layer name and position;
|
|
59
|
+
use `nodeId` when two layers share a name.
|
|
60
|
+
- If the output contains an `unresolved` array, STOP and report it to the user
|
|
61
|
+
before building. It means the packaged variables JSON is out of date and the
|
|
62
|
+
screen cannot be built faithfully. Do not guess missing modes.
|
|
63
|
+
|
|
64
|
+
## Step 3 — Build the screen
|
|
65
|
+
|
|
66
|
+
Implement with `jfs-components` components, mapping Figma component names to
|
|
67
|
+
library exports.
|
|
68
|
+
|
|
69
|
+
**Modes are non-negotiable.** Apply every extracted mode exactly as given —
|
|
70
|
+
collection name → mode name, on exactly the layer (or subtree) the extractor
|
|
71
|
+
reported. There is zero tolerance for inconsistency here:
|
|
72
|
+
|
|
73
|
+
- Do not substitute a "close enough" mode (`Emphasis: Medium` is not
|
|
74
|
+
`Emphasis: Low`).
|
|
75
|
+
- Do not drop modes that seem redundant — if the design sets
|
|
76
|
+
`AppearanceBrand: Neutral` on one Icon Button and `Appearance: Primary` on
|
|
77
|
+
another, those are different and intentional.
|
|
78
|
+
- Do not apply a child's modes to its parent or vice versa.
|
|
79
|
+
|
|
80
|
+
For everything else (spacing, order, text, icons, variants, props), follow the
|
|
81
|
+
Figma MCP output one-to-one. Reuse existing screens in the consuming app as
|
|
82
|
+
convention references for file placement, navigation registration, and asset
|
|
83
|
+
handling.
|
|
84
|
+
|
|
85
|
+
## Step 4 — QA against the design (mandatory, never skip)
|
|
86
|
+
|
|
87
|
+
After the screen compiles and renders, you MUST verify it visually against the
|
|
88
|
+
Step 1 screenshot:
|
|
89
|
+
|
|
90
|
+
- **Native app:** use the mobile MCP — launch the app, navigate to the new
|
|
91
|
+
screen, and take a screenshot (`mobile_take_screenshot`).
|
|
92
|
+
- **Web deployment:** use the browser tooling — navigate to the deployed/dev
|
|
93
|
+
URL and take a screenshot.
|
|
94
|
+
|
|
95
|
+
Compare your screenshot with the Figma screenshot side by side. Check:
|
|
96
|
+
|
|
97
|
+
1. Component sizes and emphasis levels (these are where wrong modes show up)
|
|
98
|
+
2. Colors / appearance (brand vs neutral vs secondary treatments)
|
|
99
|
+
3. Spacing, padding, and gaps
|
|
100
|
+
4. Text content and truncation
|
|
101
|
+
5. Icon choices and visibility (some layers in the design are hidden)
|
|
102
|
+
|
|
103
|
+
If there is ANY visible difference, fix it and re-screenshot. Repeat until the
|
|
104
|
+
implementation matches the design. Typical root causes, in order of
|
|
105
|
+
likelihood: a mode applied to the wrong layer, a missing mode (falling back to
|
|
106
|
+
the collection default), a wrong variant, a wrong icon.
|
|
107
|
+
|
|
108
|
+
## Summary contract
|
|
109
|
+
|
|
110
|
+
| Step | Tool | Skippable? |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| Design context + screenshot | Figma MCP | No |
|
|
113
|
+
| Mode extraction | `npx jfs-extract-modes` | No |
|
|
114
|
+
| Build | jfs-components | — |
|
|
115
|
+
| Visual QA loop | Mobile MCP or browser | No |
|
|
116
|
+
|
|
117
|
+
Report back to the user with: the screen file(s) created, the modes applied per
|
|
118
|
+
layer, and the final QA screenshot comparison.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
8
|
+
var _reactNative = require("react-native");
|
|
9
|
+
var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
|
|
10
|
+
var _JFSThemeProvider = require("../../design-tokens/JFSThemeProvider");
|
|
11
|
+
var _reactUtils = require("../../utils/react-utils");
|
|
12
|
+
var _webPlatformUtils = require("../../utils/web-platform-utils");
|
|
13
|
+
var _Badge = _interopRequireDefault(require("../Badge/Badge"));
|
|
14
|
+
var _Image = _interopRequireDefault(require("../Image/Image"));
|
|
15
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
16
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
17
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
18
|
+
const CATEGORY_CARD_STATE = 'Category Card State';
|
|
19
|
+
const BADGE_OVERLAP = 11;
|
|
20
|
+
const DEFAULT_CARD_WIDTH = 60;
|
|
21
|
+
|
|
22
|
+
// Default modes from the Figma CategoryCard component. Overridable via `modes`.
|
|
23
|
+
const CATEGORY_CARD_DEFAULT_MODES = Object.freeze({
|
|
24
|
+
AppearanceBrand: 'Tertiary',
|
|
25
|
+
'Badge Size': 'Small',
|
|
26
|
+
Context4: 'Badge',
|
|
27
|
+
"Radius": "None"
|
|
28
|
+
});
|
|
29
|
+
const toNumber = (value, fallback) => {
|
|
30
|
+
if (typeof value === 'number') {
|
|
31
|
+
return Number.isFinite(value) ? value : fallback;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
const parsed = Number(value);
|
|
35
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
36
|
+
}
|
|
37
|
+
return fallback;
|
|
38
|
+
};
|
|
39
|
+
const toFontWeight = (value, fallback) => {
|
|
40
|
+
if (typeof value === 'number') return String(value);
|
|
41
|
+
if (typeof value === 'string') return value;
|
|
42
|
+
return fallback;
|
|
43
|
+
};
|
|
44
|
+
function resolveCategoryCardTokens(modes, active) {
|
|
45
|
+
const idleModes = {
|
|
46
|
+
...modes,
|
|
47
|
+
[CATEGORY_CARD_STATE]: 'Idle'
|
|
48
|
+
};
|
|
49
|
+
const activeModes = {
|
|
50
|
+
...modes,
|
|
51
|
+
[CATEGORY_CARD_STATE]: 'Active'
|
|
52
|
+
};
|
|
53
|
+
const resolvedModes = active ? activeModes : idleModes;
|
|
54
|
+
const gap = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/gap', resolvedModes), 6);
|
|
55
|
+
const paddingHorizontal = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/padding/horizontal', resolvedModes), 8);
|
|
56
|
+
const paddingVertical = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/padding/vertical', resolvedModes), 8);
|
|
57
|
+
const radius = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/radius', resolvedModes), 8);
|
|
58
|
+
const strokeWidth = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/strokeWidth', resolvedModes), 1);
|
|
59
|
+
const strokeColor = (0, _figmaVariablesResolver.getVariableByName)('cardTab/stroke/color', resolvedModes) ?? '#f5f5f5';
|
|
60
|
+
const idleBackground = (0, _figmaVariablesResolver.getVariableByName)('cardTab/background/color', idleModes) ?? '#ffffff';
|
|
61
|
+
const activeBackground = (0, _figmaVariablesResolver.getVariableByName)('cardTab/background/color', activeModes) ?? '#f5f5f5';
|
|
62
|
+
const backgroundColor = active ? activeBackground : idleBackground;
|
|
63
|
+
const imageSize = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/image/width', resolvedModes), 36);
|
|
64
|
+
const imageRadius = toNumber((0, _figmaVariablesResolver.getVariableByName)('image/radius', resolvedModes), 0);
|
|
65
|
+
const fontFamily = (0, _figmaVariablesResolver.getVariableByName)('cardTab/label/fontFamily', resolvedModes) ?? 'JioType Var';
|
|
66
|
+
const fontSize = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/label/fontSize', resolvedModes), 12);
|
|
67
|
+
const lineHeight = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/label/lineHeight', resolvedModes), Math.round(fontSize * 1.3));
|
|
68
|
+
const fontWeight = toFontWeight((0, _figmaVariablesResolver.getVariableByName)('cardTab/label/fontWeight', resolvedModes), '500');
|
|
69
|
+
const labelColor = (0, _figmaVariablesResolver.getVariableByName)('cardTab/label/color', resolvedModes) ?? '#000000';
|
|
70
|
+
return {
|
|
71
|
+
containerStyle: {
|
|
72
|
+
position: 'relative',
|
|
73
|
+
alignItems: 'center',
|
|
74
|
+
justifyContent: 'center',
|
|
75
|
+
backgroundColor,
|
|
76
|
+
borderColor: strokeColor,
|
|
77
|
+
borderWidth: strokeWidth,
|
|
78
|
+
borderStyle: 'solid',
|
|
79
|
+
borderRadius: radius,
|
|
80
|
+
paddingHorizontal,
|
|
81
|
+
paddingTop: paddingVertical,
|
|
82
|
+
paddingBottom: paddingVertical,
|
|
83
|
+
gap
|
|
84
|
+
},
|
|
85
|
+
labelStyle: {
|
|
86
|
+
color: labelColor,
|
|
87
|
+
fontFamily,
|
|
88
|
+
fontSize,
|
|
89
|
+
fontWeight,
|
|
90
|
+
lineHeight,
|
|
91
|
+
textAlign: 'center'
|
|
92
|
+
},
|
|
93
|
+
imageSize,
|
|
94
|
+
imageRadius
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const pressedOverlayStyle = {
|
|
98
|
+
opacity: 0.85
|
|
99
|
+
};
|
|
100
|
+
const disabledOverlayStyle = {
|
|
101
|
+
opacity: 0.5
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* `CategoryCard` is a compact, tappable tile for category pickers and
|
|
106
|
+
* merchandising rails. It stacks a square image above a short label and can
|
|
107
|
+
* optionally show an overlapping badge at the top edge. Cards are typically
|
|
108
|
+
* laid out in a horizontal `ScrollView` or `HStack`.
|
|
109
|
+
*
|
|
110
|
+
* All visual values resolve through the `cardTab/*` design tokens with
|
|
111
|
+
* sensible Figma defaults so the card renders correctly out of the box.
|
|
112
|
+
*
|
|
113
|
+
* @component
|
|
114
|
+
* @param {CategoryCardProps} props
|
|
115
|
+
*/
|
|
116
|
+
function CategoryCard({
|
|
117
|
+
label = 'Popular',
|
|
118
|
+
imageSource,
|
|
119
|
+
imageSlot,
|
|
120
|
+
badge,
|
|
121
|
+
active = false,
|
|
122
|
+
width = DEFAULT_CARD_WIDTH,
|
|
123
|
+
disableTruncation,
|
|
124
|
+
singleLine,
|
|
125
|
+
onPress,
|
|
126
|
+
disabled = false,
|
|
127
|
+
modes: propModes = _reactUtils.EMPTY_MODES,
|
|
128
|
+
style,
|
|
129
|
+
labelStyle,
|
|
130
|
+
accessibilityLabel,
|
|
131
|
+
accessibilityHint,
|
|
132
|
+
accessibilityState,
|
|
133
|
+
webAccessibilityProps,
|
|
134
|
+
testID,
|
|
135
|
+
...rest
|
|
136
|
+
}) {
|
|
137
|
+
const {
|
|
138
|
+
modes: globalModes
|
|
139
|
+
} = (0, _JFSThemeProvider.useTokens)();
|
|
140
|
+
const modes = (0, _react.useMemo)(() => ({
|
|
141
|
+
...globalModes,
|
|
142
|
+
...CATEGORY_CARD_DEFAULT_MODES,
|
|
143
|
+
...propModes
|
|
144
|
+
}), [globalModes, propModes]);
|
|
145
|
+
const resolvedModes = (0, _react.useMemo)(() => ({
|
|
146
|
+
...modes,
|
|
147
|
+
[CATEGORY_CARD_STATE]: active ? 'Active' : 'Idle'
|
|
148
|
+
}), [modes, active]);
|
|
149
|
+
const tokens = (0, _react.useMemo)(() => resolveCategoryCardTokens(resolvedModes, active), [resolvedModes, active]);
|
|
150
|
+
const {
|
|
151
|
+
style: labelLayoutStyle,
|
|
152
|
+
...labelTruncation
|
|
153
|
+
} = (0, _react.useMemo)(() => (0, _reactUtils.resolveTextLayout)({
|
|
154
|
+
disableTruncation,
|
|
155
|
+
singleLine,
|
|
156
|
+
numberOfLines: 1,
|
|
157
|
+
ellipsizeMode: 'tail'
|
|
158
|
+
}), [disableTruncation, singleLine]);
|
|
159
|
+
const processedImageSlot = (0, _react.useMemo)(() => {
|
|
160
|
+
if (!imageSlot) return null;
|
|
161
|
+
const processed = (0, _reactUtils.cloneChildrenWithModes)(imageSlot, resolvedModes);
|
|
162
|
+
return processed.length === 1 ? processed[0] : processed;
|
|
163
|
+
}, [imageSlot, resolvedModes]);
|
|
164
|
+
const imageNode = processedImageSlot ?? (imageSource !== undefined ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Image.default, {
|
|
165
|
+
imageSource: imageSource,
|
|
166
|
+
width: tokens.imageSize,
|
|
167
|
+
height: tokens.imageSize,
|
|
168
|
+
ratio: 1,
|
|
169
|
+
resizeMode: "cover",
|
|
170
|
+
borderRadius: tokens.imageRadius,
|
|
171
|
+
accessibilityElementsHidden: true,
|
|
172
|
+
importantForAccessibility: "no"
|
|
173
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_Image.default, {
|
|
174
|
+
imageSource: "https://www.figma.com/api/mcp/asset/0ee34871-646f-4736-be06-22fb6c0a4eed",
|
|
175
|
+
width: tokens.imageSize,
|
|
176
|
+
height: tokens.imageSize,
|
|
177
|
+
ratio: 1,
|
|
178
|
+
resizeMode: "cover",
|
|
179
|
+
borderRadius: tokens.imageRadius,
|
|
180
|
+
accessibilityElementsHidden: true,
|
|
181
|
+
importantForAccessibility: "no"
|
|
182
|
+
}));
|
|
183
|
+
const a11yLabel = accessibilityLabel ?? (badge ? `${label}, ${badge}` : label);
|
|
184
|
+
const webProps = (0, _webPlatformUtils.usePressableWebSupport)({
|
|
185
|
+
restProps: rest,
|
|
186
|
+
onPress: disabled ? undefined : onPress,
|
|
187
|
+
disabled,
|
|
188
|
+
accessibilityLabel: a11yLabel,
|
|
189
|
+
webAccessibilityProps
|
|
190
|
+
});
|
|
191
|
+
const badgeNode = badge ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
192
|
+
style: BADGE_ANCHOR,
|
|
193
|
+
pointerEvents: "none",
|
|
194
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Badge.default, {
|
|
195
|
+
label: badge,
|
|
196
|
+
modes: resolvedModes,
|
|
197
|
+
style: BADGE_CENTER_STYLE
|
|
198
|
+
})
|
|
199
|
+
}) : null;
|
|
200
|
+
|
|
201
|
+
// Truncation needs a width bound so the ellipsis engages. `singleLine`
|
|
202
|
+
// must NOT fill the content box — otherwise the text is pinned to the left
|
|
203
|
+
// padding edge and only grows rightward. Intrinsic width + centered
|
|
204
|
+
// alignment lets overflow expand equally on both sides.
|
|
205
|
+
const labelWidthStyle = singleLine ? LABEL_OVERFLOW_CENTER_STYLE : LABEL_FILL_STYLE;
|
|
206
|
+
const pressableStyle = (0, _react.useCallback)(({
|
|
207
|
+
pressed
|
|
208
|
+
}) => [tokens.containerStyle, {
|
|
209
|
+
width
|
|
210
|
+
}, badge ? {
|
|
211
|
+
marginTop: BADGE_OVERLAP
|
|
212
|
+
} : null, disabled ? disabledOverlayStyle : null, pressed && !disabled ? pressedOverlayStyle : null, style], [tokens.containerStyle, width, badge, disabled, style]);
|
|
213
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
|
|
214
|
+
accessibilityRole: "button",
|
|
215
|
+
accessibilityLabel: a11yLabel,
|
|
216
|
+
accessibilityHint: accessibilityHint,
|
|
217
|
+
accessibilityState: {
|
|
218
|
+
disabled,
|
|
219
|
+
selected: active,
|
|
220
|
+
...accessibilityState
|
|
221
|
+
},
|
|
222
|
+
onPress: onPress,
|
|
223
|
+
disabled: disabled,
|
|
224
|
+
style: pressableStyle,
|
|
225
|
+
testID: testID,
|
|
226
|
+
...webProps,
|
|
227
|
+
children: [badgeNode, imageNode, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
228
|
+
style: [tokens.labelStyle, labelWidthStyle, labelLayoutStyle, labelStyle],
|
|
229
|
+
...labelTruncation,
|
|
230
|
+
accessibilityElementsHidden: true,
|
|
231
|
+
importantForAccessibility: "no",
|
|
232
|
+
children: label
|
|
233
|
+
})]
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const BADGE_ANCHOR = {
|
|
237
|
+
position: 'absolute',
|
|
238
|
+
top: -BADGE_OVERLAP,
|
|
239
|
+
left: 0,
|
|
240
|
+
right: 0,
|
|
241
|
+
alignItems: 'center',
|
|
242
|
+
justifyContent: 'center',
|
|
243
|
+
zIndex: 1
|
|
244
|
+
};
|
|
245
|
+
const BADGE_CENTER_STYLE = {
|
|
246
|
+
alignSelf: 'center'
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/** Lets the label fill the card width so the one-line ellipsis has a bound. */
|
|
250
|
+
const LABEL_FILL_STYLE = {
|
|
251
|
+
width: '100%',
|
|
252
|
+
alignSelf: 'stretch'
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Intrinsic width, horizontally centred. When the label is wider than the
|
|
257
|
+
* card it overflows left and right equally instead of growing only rightward
|
|
258
|
+
* from the padded content edge.
|
|
259
|
+
*/
|
|
260
|
+
const LABEL_OVERFLOW_CENTER_STYLE = {
|
|
261
|
+
alignSelf: 'center',
|
|
262
|
+
textAlign: 'center'
|
|
263
|
+
};
|
|
264
|
+
var _default = exports.default = /*#__PURE__*/_react.default.memo(CategoryCard);
|