@colixsystems/widget-sdk 0.162.0 → 0.163.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 +16 -1
- package/dist/contract.cjs +43 -1
- package/dist/contract.js +43 -1
- package/dist/index.d.ts +29 -0
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/tabs-tokens.js +75 -0
- package/dist/tabs-view.js +169 -0
- package/dist/tabs.js +7 -0
- package/dist/tabs.native.js +7 -0
- package/dist/theme-components.cjs +5 -0
- package/dist/theme-components.js +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,7 +73,21 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
73
73
|
|
|
74
74
|
## Status
|
|
75
75
|
|
|
76
|
-
`v0.
|
|
76
|
+
`v0.163.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**.
|
|
77
|
+
|
|
78
|
+
### What's new in 0.163.0 (contract 1.126.0)
|
|
79
|
+
|
|
80
|
+
**A themed tab strip: `Tabs`, and the `tabs` theme scope.** `<Tabs items={[{ key, label, icon? }]} value={tab} onChange={setTab} />` draws a strip whose look — a segmented control, pills, or an underline — and whose colours and corners come from **Theme Settings > Tabs** (`themeConfig.components.tabs`). It draws the strip only; render the selected panel yourself. The page Tabs container now renders through the same component on the web Player and in the exported app, so a page's tabs and a widget's tabs match, and the two hosts no longer draw different strips. Unthemed, the strip is the segmented control the exported app always drew.
|
|
81
|
+
|
|
82
|
+
```jsx
|
|
83
|
+
const [tab, setTab] = useState("open");
|
|
84
|
+
<Tabs items={[{ key: "open", label: "Open" }, { key: "done", label: "Done", icon: "check" }]} value={tab} onChange={setTab} />
|
|
85
|
+
{tab === "open" ? <OpenOrders /> : <DoneOrders />}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Pass `variant` ("segmented" | "pill" | "underline"), `activeColor`, `activeTextColor`, `textColor` or `radius` to override one strip, or declare the style fields `tabsStyle`, `tabsActiveColor`, `tabsActiveTextColor`, `tabsTextColor` and `tabsRadius` so an author can.
|
|
89
|
+
|
|
90
|
+
The component-token vocabulary gains an **`enum`** type (`{ type: "enum", values }`), first used by `tabs.style`; `CONTRACT.themeComponentTabStyles` lists the styles.
|
|
77
91
|
|
|
78
92
|
### What's new in 0.162.0 (contract 1.125.0)
|
|
79
93
|
|
|
@@ -1787,6 +1801,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
1787
1801
|
- `parseMarkdown`, `markdownToPlainText`, `parseMarkdownImage`, `formatMarkdownImage`, `stripHtmlToMarkdown`, `MARKDOWN_IMAGE_SIZES` — the same grammar as pure helpers, for content you inspect rather than render: the block list, a marker-free projection (list previews, search, a11y labels), the one-line image form read and written, a legacy-HTML row normalised to markdown on read, and the closed size list (`small` | `medium` | `large` | `full`).
|
|
1788
1802
|
- `isSafeMarkdownImageSrc`, `isHttpImageSrc`, `normaliseMarkdownImageSize`, `readMarkdownAlt` — the image half of that grammar, for a widget rendering images itself via `renderImage`. `isSafeMarkdownImageSrc` is the same allowlist the parser applies (an `http(s)` URL or a filestore id; a `javascript:` scheme is refused), so the widget and the parser cannot disagree about which src is renderable.
|
|
1789
1803
|
- `List`, `ListRow`, `EmptyState` — the themed list building blocks. `<ListRow title subtitle leading trailing onPress selected divider>` draws one record row from the theme's `list` scope (padding, height, gap, radius, background, divider); `<List>` spaces its rows and hands overrides down; `<EmptyState icon title message>` is the "nothing here" panel. Never hand-roll a row from `View` + `Pressable`.
|
|
1804
|
+
- `Tabs` — the themed tab strip. `<Tabs items value onChange>` draws a segmented control, pills or an underline as the theme's `tabs` scope says; render the selected panel yourself. Never hand-roll tabs from `Pressable` chips.
|
|
1790
1805
|
- `textRoleStyle(role, typography, colors)`, `TEXT_ROLES` — the style of a semantic text tier, shared with the built-in widgets.
|
|
1791
1806
|
- `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
|
|
1792
1807
|
|
package/dist/contract.cjs
CHANGED
|
@@ -228,6 +228,9 @@ function isThemeColorTokenRef(value) {
|
|
|
228
228
|
// Single source for four consumers — the build runner's set_theme coercion, the
|
|
229
229
|
// shared host resolver, the planner prompt, and the SDK docs — so the vocabulary
|
|
230
230
|
// cannot drift between what Mason may emit and what a host actually applies.
|
|
231
|
+
// The looks a `tabs.style` token may name; `<Tabs>` draws each.
|
|
232
|
+
const THEME_TAB_STYLES = Object.freeze(["segmented", "pill", "underline"]);
|
|
233
|
+
|
|
231
234
|
const THEME_COMPONENT_SHADOWS = Object.freeze(["none", "sm", "md", "lg"]);
|
|
232
235
|
|
|
233
236
|
// sc-3857 — the `textTransform` token's closed enum, matching the per-instance
|
|
@@ -445,6 +448,16 @@ const LIST_ROW_FIELDS = Object.freeze({
|
|
|
445
448
|
dividerWidth: "listDividerWidth",
|
|
446
449
|
});
|
|
447
450
|
|
|
451
|
+
// The tab-strip vocabulary. Bound by name as well as onto User Management,
|
|
452
|
+
// whose Users / Groups / Invites strip is drawn by the SDK <Tabs>.
|
|
453
|
+
const TABS_FIELDS = Object.freeze({
|
|
454
|
+
style: "tabsStyle",
|
|
455
|
+
activeColor: "tabsActiveColor",
|
|
456
|
+
activeTextColor: "tabsActiveTextColor",
|
|
457
|
+
textColor: "tabsTextColor",
|
|
458
|
+
radius: "tabsRadius",
|
|
459
|
+
});
|
|
460
|
+
|
|
448
461
|
const THEME_COMPONENTS = Object.freeze({
|
|
449
462
|
button: Object.freeze({
|
|
450
463
|
label: "Buttons",
|
|
@@ -614,6 +627,22 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
614
627
|
"appstudio.data-list": LIST_ROW_FIELDS,
|
|
615
628
|
}),
|
|
616
629
|
}),
|
|
630
|
+
// Tab strips: `<Tabs>` and the page Tabs container read this scope directly;
|
|
631
|
+
// the fields bind it onto widgets. `style` is the first `enum` token.
|
|
632
|
+
tabs: Object.freeze({
|
|
633
|
+
label: "Tabs",
|
|
634
|
+
universalFields: TABS_FIELDS,
|
|
635
|
+
tokens: Object.freeze({
|
|
636
|
+
style: Object.freeze({ type: "enum", values: THEME_TAB_STYLES }),
|
|
637
|
+
activeColor: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
|
|
638
|
+
activeTextColor: Object.freeze({ type: "color" }),
|
|
639
|
+
textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurfaceMuted" }),
|
|
640
|
+
radius: Object.freeze({ type: "size", min: 0, max: 24 }),
|
|
641
|
+
}),
|
|
642
|
+
targets: Object.freeze({
|
|
643
|
+
"appstudio.user-management": TABS_FIELDS,
|
|
644
|
+
}),
|
|
645
|
+
}),
|
|
617
646
|
});
|
|
618
647
|
|
|
619
648
|
// sc-4505 — the value types a widget event payload field may declare. Narrower
|
|
@@ -2224,6 +2253,13 @@ const PRIMITIVES = [
|
|
|
2224
2253
|
rnComponent: null,
|
|
2225
2254
|
docsUrl: null,
|
|
2226
2255
|
},
|
|
2256
|
+
{
|
|
2257
|
+
name: "Tabs",
|
|
2258
|
+
description:
|
|
2259
|
+
'Themed tab strip. `<Tabs items={[{ key: "open", label: "Open" }, { key: "done", label: "Done", icon: "check" }]} value={tab} onChange={setTab} />`. THE way to switch between views inside a widget: its look (a segmented control, pills, or an underline — Theme Settings > Tabs) and its colours and corner radius come from the theme, so every tab strip in the app matches, including the page Tabs container. It draws the strip only; render the selected panel yourself below it. Never hand-roll tabs from `Pressable` chips. Props: `items` (`[{ key, label, icon?, disabled?, testID? }]`, icon is a lucide name), `value` (the selected key), `onChange(key)`, `accessibilityLabel`, `style`, `testID`, and the overrides `variant` ("segmented" | "pill" | "underline"), `activeColor`, `activeTextColor`, `textColor`, `radius`. The strip scrolls sideways when the tabs overflow. Declare `tabsStyle` / `tabsActiveColor` / `tabsActiveTextColor` / `tabsTextColor` / `tabsRadius` style fields and pass them through to let an author override one widget.',
|
|
2260
|
+
rnComponent: null,
|
|
2261
|
+
docsUrl: null,
|
|
2262
|
+
},
|
|
2227
2263
|
];
|
|
2228
2264
|
|
|
2229
2265
|
const CATEGORIES = [
|
|
@@ -4413,7 +4449,12 @@ const CONTRACT = deepFreeze({
|
|
|
4413
4449
|
// primitives that read it, plus `textRoleStyle` and its role helpers.
|
|
4414
4450
|
// Widgets each hand-rolled their rows, so two lists in one app disagreed.
|
|
4415
4451
|
// Additive: a theme without `list` paints the rows exactly as before.
|
|
4416
|
-
|
|
4452
|
+
// 1.126.0: additive — the `tabs` component scope and the `Tabs` primitive
|
|
4453
|
+
// that reads it, and the `enum` component-token type (`values`), first
|
|
4454
|
+
// used by `tabs.style`. The page Tabs container renders through `Tabs` on
|
|
4455
|
+
// both hosts. Additive: an unthemed strip is the segmented control the
|
|
4456
|
+
// export always drew.
|
|
4457
|
+
version: "1.126.0",
|
|
4417
4458
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4418
4459
|
hooks: HOOKS,
|
|
4419
4460
|
primitives: PRIMITIVES,
|
|
@@ -4426,6 +4467,7 @@ const CONTRACT = deepFreeze({
|
|
|
4426
4467
|
themeTokens: DEFAULT_THEME_TOKENS,
|
|
4427
4468
|
themeColorTokens: THEME_COLOR_TOKENS,
|
|
4428
4469
|
themeComponents: THEME_COMPONENTS,
|
|
4470
|
+
themeComponentTabStyles: THEME_TAB_STYLES,
|
|
4429
4471
|
themeComponentShadows: THEME_COMPONENT_SHADOWS,
|
|
4430
4472
|
themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
|
|
4431
4473
|
themeComponentGradient: THEME_COMPONENT_GRADIENT,
|
package/dist/contract.js
CHANGED
|
@@ -228,6 +228,9 @@ function isThemeColorTokenRef(value) {
|
|
|
228
228
|
// Single source for four consumers — the build runner's set_theme coercion, the
|
|
229
229
|
// shared host resolver, the planner prompt, and the SDK docs — so the vocabulary
|
|
230
230
|
// cannot drift between what Mason may emit and what a host actually applies.
|
|
231
|
+
// The looks a `tabs.style` token may name; `<Tabs>` draws each.
|
|
232
|
+
const THEME_TAB_STYLES = Object.freeze(["segmented", "pill", "underline"]);
|
|
233
|
+
|
|
231
234
|
const THEME_COMPONENT_SHADOWS = Object.freeze(["none", "sm", "md", "lg"]);
|
|
232
235
|
|
|
233
236
|
// sc-3857 — the `textTransform` token's closed enum, matching the per-instance
|
|
@@ -445,6 +448,16 @@ const LIST_ROW_FIELDS = Object.freeze({
|
|
|
445
448
|
dividerWidth: "listDividerWidth",
|
|
446
449
|
});
|
|
447
450
|
|
|
451
|
+
// The tab-strip vocabulary. Bound by name as well as onto User Management,
|
|
452
|
+
// whose Users / Groups / Invites strip is drawn by the SDK <Tabs>.
|
|
453
|
+
const TABS_FIELDS = Object.freeze({
|
|
454
|
+
style: "tabsStyle",
|
|
455
|
+
activeColor: "tabsActiveColor",
|
|
456
|
+
activeTextColor: "tabsActiveTextColor",
|
|
457
|
+
textColor: "tabsTextColor",
|
|
458
|
+
radius: "tabsRadius",
|
|
459
|
+
});
|
|
460
|
+
|
|
448
461
|
const THEME_COMPONENTS = Object.freeze({
|
|
449
462
|
button: Object.freeze({
|
|
450
463
|
label: "Buttons",
|
|
@@ -614,6 +627,22 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
614
627
|
"appstudio.data-list": LIST_ROW_FIELDS,
|
|
615
628
|
}),
|
|
616
629
|
}),
|
|
630
|
+
// Tab strips: `<Tabs>` and the page Tabs container read this scope directly;
|
|
631
|
+
// the fields bind it onto widgets. `style` is the first `enum` token.
|
|
632
|
+
tabs: Object.freeze({
|
|
633
|
+
label: "Tabs",
|
|
634
|
+
universalFields: TABS_FIELDS,
|
|
635
|
+
tokens: Object.freeze({
|
|
636
|
+
style: Object.freeze({ type: "enum", values: THEME_TAB_STYLES }),
|
|
637
|
+
activeColor: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
|
|
638
|
+
activeTextColor: Object.freeze({ type: "color" }),
|
|
639
|
+
textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurfaceMuted" }),
|
|
640
|
+
radius: Object.freeze({ type: "size", min: 0, max: 24 }),
|
|
641
|
+
}),
|
|
642
|
+
targets: Object.freeze({
|
|
643
|
+
"appstudio.user-management": TABS_FIELDS,
|
|
644
|
+
}),
|
|
645
|
+
}),
|
|
617
646
|
});
|
|
618
647
|
|
|
619
648
|
// sc-4505 — the value types a widget event payload field may declare. Narrower
|
|
@@ -2224,6 +2253,13 @@ const PRIMITIVES = [
|
|
|
2224
2253
|
rnComponent: null,
|
|
2225
2254
|
docsUrl: null,
|
|
2226
2255
|
},
|
|
2256
|
+
{
|
|
2257
|
+
name: "Tabs",
|
|
2258
|
+
description:
|
|
2259
|
+
'Themed tab strip. `<Tabs items={[{ key: "open", label: "Open" }, { key: "done", label: "Done", icon: "check" }]} value={tab} onChange={setTab} />`. THE way to switch between views inside a widget: its look (a segmented control, pills, or an underline — Theme Settings > Tabs) and its colours and corner radius come from the theme, so every tab strip in the app matches, including the page Tabs container. It draws the strip only; render the selected panel yourself below it. Never hand-roll tabs from `Pressable` chips. Props: `items` (`[{ key, label, icon?, disabled?, testID? }]`, icon is a lucide name), `value` (the selected key), `onChange(key)`, `accessibilityLabel`, `style`, `testID`, and the overrides `variant` ("segmented" | "pill" | "underline"), `activeColor`, `activeTextColor`, `textColor`, `radius`. The strip scrolls sideways when the tabs overflow. Declare `tabsStyle` / `tabsActiveColor` / `tabsActiveTextColor` / `tabsTextColor` / `tabsRadius` style fields and pass them through to let an author override one widget.',
|
|
2260
|
+
rnComponent: null,
|
|
2261
|
+
docsUrl: null,
|
|
2262
|
+
},
|
|
2227
2263
|
];
|
|
2228
2264
|
|
|
2229
2265
|
const CATEGORIES = [
|
|
@@ -4409,7 +4445,12 @@ const CONTRACT = deepFreeze({
|
|
|
4409
4445
|
// primitives that read it, plus `textRoleStyle` and its role helpers.
|
|
4410
4446
|
// Widgets each hand-rolled their rows, so two lists in one app disagreed.
|
|
4411
4447
|
// Additive: a theme without `list` paints the rows exactly as before.
|
|
4412
|
-
|
|
4448
|
+
// 1.126.0: additive — the `tabs` component scope and the `Tabs` primitive
|
|
4449
|
+
// that reads it, and the `enum` component-token type (`values`), first
|
|
4450
|
+
// used by `tabs.style`. The page Tabs container renders through `Tabs` on
|
|
4451
|
+
// both hosts. Additive: an unthemed strip is the segmented control the
|
|
4452
|
+
// export always drew.
|
|
4453
|
+
version: "1.126.0",
|
|
4413
4454
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4414
4455
|
hooks: HOOKS,
|
|
4415
4456
|
primitives: PRIMITIVES,
|
|
@@ -4422,6 +4463,7 @@ const CONTRACT = deepFreeze({
|
|
|
4422
4463
|
themeTokens: DEFAULT_THEME_TOKENS,
|
|
4423
4464
|
themeColorTokens: THEME_COLOR_TOKENS,
|
|
4424
4465
|
themeComponents: THEME_COMPONENTS,
|
|
4466
|
+
themeComponentTabStyles: THEME_TAB_STYLES,
|
|
4425
4467
|
themeComponentShadows: THEME_COMPONENT_SHADOWS,
|
|
4426
4468
|
themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
|
|
4427
4469
|
themeComponentGradient: THEME_COMPONENT_GRADIENT,
|
package/dist/index.d.ts
CHANGED
|
@@ -2615,6 +2615,35 @@ export const EmptyState: (props: {
|
|
|
2615
2615
|
children?: ReactNode;
|
|
2616
2616
|
}) => any;
|
|
2617
2617
|
|
|
2618
|
+
export type TabStyle = "segmented" | "pill" | "underline";
|
|
2619
|
+
|
|
2620
|
+
/**
|
|
2621
|
+
* Themed tab strip. Its look (segmented, pills or underline), colours and
|
|
2622
|
+
* corners come from the theme's `tabs` scope, so every strip in the app —
|
|
2623
|
+
* including the page Tabs container — matches. Draws the strip only; render
|
|
2624
|
+
* the selected panel yourself.
|
|
2625
|
+
*
|
|
2626
|
+
* @example
|
|
2627
|
+
* const [tab, setTab] = useState("open");
|
|
2628
|
+
* <Tabs items={[{ key: "open", label: "Open" }, { key: "done", label: "Done" }]} value={tab} onChange={setTab} />
|
|
2629
|
+
*/
|
|
2630
|
+
export const Tabs: (props: {
|
|
2631
|
+
items: Array<{ key: string; label: ReactNode; icon?: string; disabled?: boolean; testID?: string }>;
|
|
2632
|
+
value?: string;
|
|
2633
|
+
onChange?: (key: string) => void;
|
|
2634
|
+
/** Overrides the theme's tab style for this strip. */
|
|
2635
|
+
variant?: TabStyle;
|
|
2636
|
+
activeColor?: string;
|
|
2637
|
+
activeTextColor?: string;
|
|
2638
|
+
textColor?: string;
|
|
2639
|
+
radius?: number;
|
|
2640
|
+
/** For hosts rendering outside a widget context; widgets omit it. */
|
|
2641
|
+
theme?: any;
|
|
2642
|
+
accessibilityLabel?: string;
|
|
2643
|
+
style?: any;
|
|
2644
|
+
testID?: string;
|
|
2645
|
+
}) => any;
|
|
2646
|
+
|
|
2618
2647
|
export type TextRole = "display" | "title" | "subtitle" | "body" | "caption" | "overline";
|
|
2619
2648
|
export const TEXT_ROLES: readonly TextRole[];
|
|
2620
2649
|
export const TEXT_TRANSFORMS: readonly string[];
|
package/dist/index.js
CHANGED
|
@@ -191,6 +191,8 @@ export {
|
|
|
191
191
|
export { insertAtCaret, pushRecentEmoji } from "./emoji-picker-tokens.js";
|
|
192
192
|
// Themed list building blocks; rows follow the theme's `list` scope.
|
|
193
193
|
export { List, ListRow, EmptyState } from "./list.js";
|
|
194
|
+
// A themed tab strip; follows the theme's `tabs` scope.
|
|
195
|
+
export { Tabs } from "./tabs.js";
|
|
194
196
|
// The semantic text tiers, shared with the built-in widgets.
|
|
195
197
|
export {
|
|
196
198
|
textRoleStyle,
|
package/dist/index.native.js
CHANGED
|
@@ -184,6 +184,8 @@ export {
|
|
|
184
184
|
export { insertAtCaret, pushRecentEmoji } from "./emoji-picker-tokens.js";
|
|
185
185
|
// Themed list building blocks; rows follow the theme's `list` scope.
|
|
186
186
|
export { List, ListRow, EmptyState } from "./list.native.js";
|
|
187
|
+
// A themed tab strip; follows the theme's `tabs` scope.
|
|
188
|
+
export { Tabs } from "./tabs.native.js";
|
|
187
189
|
// The semantic text tiers, shared with the built-in widgets.
|
|
188
190
|
export {
|
|
189
191
|
textRoleStyle,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The values `<Tabs>` paints with, resolved from the theme's `tabs` component
|
|
2
|
+
// scope. Platform-free, like list-tokens.js, so both hosts draw one strip from
|
|
3
|
+
// the same numbers and tests can pin them without a renderer.
|
|
4
|
+
|
|
5
|
+
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
6
|
+
import { CONTRACT, contrastRatio } from "./contract.js";
|
|
7
|
+
|
|
8
|
+
export const TAB_STYLES = CONTRACT.themeComponentTabStyles;
|
|
9
|
+
|
|
10
|
+
// Segmented is the strip the exported app's Tabs container always drew.
|
|
11
|
+
export const DEFAULT_TAB_STYLE = "segmented";
|
|
12
|
+
export const DEFAULT_TAB_RADIUS = 6;
|
|
13
|
+
|
|
14
|
+
// Per-strip overrides a widget may pass, named as the theme tokens are.
|
|
15
|
+
export const TAB_TOKEN_NAMES = Object.freeze([
|
|
16
|
+
"style",
|
|
17
|
+
"activeColor",
|
|
18
|
+
"activeTextColor",
|
|
19
|
+
"textColor",
|
|
20
|
+
"radius",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function isSet(value) {
|
|
24
|
+
return value !== undefined && value !== null && value !== "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function pick(overrides, scope, name) {
|
|
28
|
+
if (overrides && isSet(overrides[name])) return overrides[name];
|
|
29
|
+
if (scope && isSet(scope[name])) return scope[name];
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve one tab strip: explicit override, then the theme's `tabs` scope,
|
|
35
|
+
* then the widget's `defaultStyle`, then the tokens. The active label is the
|
|
36
|
+
* accent on the surface styles, and readable on the accent for a pill.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} theme — the resolved workspace theme (`useTheme()` shape).
|
|
39
|
+
* @param {object} [overrides] — per-strip values keyed by TAB_TOKEN_NAMES.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveTabsTokens(theme, overrides) {
|
|
42
|
+
const base = theme && typeof theme === "object" ? theme : DEFAULT_THEME_TOKENS;
|
|
43
|
+
const colors = base.colors || DEFAULT_THEME_TOKENS.colors;
|
|
44
|
+
const scope = base.components && base.components.tabs;
|
|
45
|
+
|
|
46
|
+
// The widget's own default sits BELOW the theme: a look still wins.
|
|
47
|
+
const requested =
|
|
48
|
+
pick(overrides, scope, "style") ?? (overrides && overrides.defaultStyle);
|
|
49
|
+
const style = TAB_STYLES.includes(requested) ? requested : DEFAULT_TAB_STYLE;
|
|
50
|
+
const ownAccent = pick(overrides, scope, "activeColor");
|
|
51
|
+
const activeColor = ownAccent ?? colors.primary;
|
|
52
|
+
return {
|
|
53
|
+
style,
|
|
54
|
+
activeColor,
|
|
55
|
+
activeTextColor:
|
|
56
|
+
pick(overrides, scope, "activeTextColor") ??
|
|
57
|
+
(style === "pill"
|
|
58
|
+
? ownAccent
|
|
59
|
+
? readableOn(ownAccent, colors)
|
|
60
|
+
: colors.onPrimary
|
|
61
|
+
: activeColor),
|
|
62
|
+
textColor: pick(overrides, scope, "textColor") ?? colors.onSurfaceMuted,
|
|
63
|
+
radius: pick(overrides, scope, "radius") ?? DEFAULT_TAB_RADIUS,
|
|
64
|
+
barBackground: colors.surfaceMuted,
|
|
65
|
+
activeSurface: colors.surface,
|
|
66
|
+
rule: colors.border,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A chosen accent carries the label that reads best on it, dark or light.
|
|
71
|
+
function readableOn(fill, colors) {
|
|
72
|
+
const dark = colors.onSurface;
|
|
73
|
+
const light = "#ffffff";
|
|
74
|
+
return contrastRatio(fill, dark) >= contrastRatio(fill, light) ? dark : light;
|
|
75
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// The ONE implementation behind the `<Tabs>` SDK primitive. The platform
|
|
2
|
+
// bindings (`tabs.js` / `tabs.native.js`) differ only in where the primitives
|
|
3
|
+
// come from, so the hosts cannot drift.
|
|
4
|
+
//
|
|
5
|
+
// The page Tabs container, the built-in widgets and custom widgets each drew
|
|
6
|
+
// their own tab strip, so an app's tabs looked different on every screen and
|
|
7
|
+
// between web and native. This strip reads the theme's `tabs` scope.
|
|
8
|
+
|
|
9
|
+
import React from "react";
|
|
10
|
+
import { useHostTheme } from "./hooks.js";
|
|
11
|
+
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
12
|
+
import { Icon } from "./icon.js";
|
|
13
|
+
import { resolveTabsTokens } from "./tabs-tokens.js";
|
|
14
|
+
|
|
15
|
+
const SEGMENT_INSET = 4;
|
|
16
|
+
|
|
17
|
+
function stripStyles(t, theme) {
|
|
18
|
+
const spacing = theme.spacing || DEFAULT_THEME_TOKENS.spacing;
|
|
19
|
+
const elevation = theme.elevation || DEFAULT_THEME_TOKENS.elevation;
|
|
20
|
+
if (t.style === "pill") {
|
|
21
|
+
// An idle chip is outlined: a muted fill vanishes on a white card.
|
|
22
|
+
return {
|
|
23
|
+
bar: null,
|
|
24
|
+
content: { gap: spacing.sm },
|
|
25
|
+
item: (active) => ({
|
|
26
|
+
borderRadius: t.radius,
|
|
27
|
+
borderWidth: 1,
|
|
28
|
+
borderColor: active ? t.activeColor : t.rule,
|
|
29
|
+
backgroundColor: active ? t.activeColor : "transparent",
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (t.style === "underline") {
|
|
34
|
+
// The rule sits on the content, so the scroller cannot clip the accent.
|
|
35
|
+
return {
|
|
36
|
+
bar: null,
|
|
37
|
+
content: { gap: spacing.lg, borderBottomWidth: 1, borderColor: t.rule },
|
|
38
|
+
item: (active) => ({
|
|
39
|
+
paddingHorizontal: 4,
|
|
40
|
+
borderBottomWidth: 2,
|
|
41
|
+
borderColor: active ? t.activeColor : "transparent",
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// A concentric outer radius keeps the inset chip's corners parallel. The
|
|
46
|
+
// bar hugs its segments, and the active one is outlined as well as lifted
|
|
47
|
+
// because a shadow alone disappears on a dark theme.
|
|
48
|
+
return {
|
|
49
|
+
bar: {
|
|
50
|
+
backgroundColor: t.barBackground,
|
|
51
|
+
borderRadius: t.radius + 2,
|
|
52
|
+
alignSelf: "flex-start",
|
|
53
|
+
maxWidth: "100%",
|
|
54
|
+
},
|
|
55
|
+
content: { padding: SEGMENT_INSET, gap: SEGMENT_INSET },
|
|
56
|
+
item: (active) => ({
|
|
57
|
+
borderRadius: t.radius,
|
|
58
|
+
borderWidth: 1,
|
|
59
|
+
borderColor: active ? t.rule : "transparent",
|
|
60
|
+
...(active
|
|
61
|
+
? { backgroundColor: t.activeSurface, ...(elevation.sm || {}) }
|
|
62
|
+
: null),
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Bind the shared tab strip to one platform's primitives.
|
|
69
|
+
*
|
|
70
|
+
* @param {{View: any, Text: any, Pressable: any, ScrollView: any}} rn
|
|
71
|
+
*/
|
|
72
|
+
export function makeTabs(rn) {
|
|
73
|
+
const { Text, Pressable, ScrollView } = rn;
|
|
74
|
+
|
|
75
|
+
function Tabs({
|
|
76
|
+
items = [],
|
|
77
|
+
value,
|
|
78
|
+
onChange,
|
|
79
|
+
variant,
|
|
80
|
+
defaultVariant,
|
|
81
|
+
activeColor,
|
|
82
|
+
activeTextColor,
|
|
83
|
+
textColor,
|
|
84
|
+
radius,
|
|
85
|
+
theme: explicitTheme,
|
|
86
|
+
accessibilityLabel,
|
|
87
|
+
style,
|
|
88
|
+
testID,
|
|
89
|
+
}) {
|
|
90
|
+
const hostTheme = useHostTheme();
|
|
91
|
+
// Layout containers render outside a widget context, so they pass theirs.
|
|
92
|
+
const theme = explicitTheme || hostTheme || DEFAULT_THEME_TOKENS;
|
|
93
|
+
const t = resolveTabsTokens(theme, {
|
|
94
|
+
defaultStyle: defaultVariant,
|
|
95
|
+
style: variant,
|
|
96
|
+
activeColor,
|
|
97
|
+
activeTextColor,
|
|
98
|
+
textColor,
|
|
99
|
+
radius,
|
|
100
|
+
});
|
|
101
|
+
const typography = theme.typography || DEFAULT_THEME_TOKENS.typography;
|
|
102
|
+
const strip = stripStyles(t, theme);
|
|
103
|
+
|
|
104
|
+
return React.createElement(
|
|
105
|
+
ScrollView,
|
|
106
|
+
{
|
|
107
|
+
horizontal: true,
|
|
108
|
+
showsHorizontalScrollIndicator: false,
|
|
109
|
+
// A strip never grows into, or shrinks away from, the panel below.
|
|
110
|
+
style: [{ flexGrow: 0, flexShrink: 0 }, strip.bar, style],
|
|
111
|
+
contentContainerStyle: [{ flexDirection: "row" }, strip.content],
|
|
112
|
+
accessibilityRole: "tablist",
|
|
113
|
+
accessibilityLabel,
|
|
114
|
+
testID,
|
|
115
|
+
},
|
|
116
|
+
...items.map((item) => {
|
|
117
|
+
const active = item.key === value;
|
|
118
|
+
const ink = active ? t.activeTextColor : t.textColor;
|
|
119
|
+
return React.createElement(
|
|
120
|
+
Pressable,
|
|
121
|
+
{
|
|
122
|
+
key: item.key,
|
|
123
|
+
onPress: () => {
|
|
124
|
+
if (!active && typeof onChange === "function") onChange(item.key);
|
|
125
|
+
},
|
|
126
|
+
disabled: item.disabled,
|
|
127
|
+
// accessibilityRole + aria-*: the pair that announces on both hosts.
|
|
128
|
+
accessibilityRole: "tab",
|
|
129
|
+
"aria-selected": active,
|
|
130
|
+
"aria-disabled": !!item.disabled,
|
|
131
|
+
testID: item.testID,
|
|
132
|
+
style: [
|
|
133
|
+
{
|
|
134
|
+
flexDirection: "row",
|
|
135
|
+
alignItems: "center",
|
|
136
|
+
justifyContent: "center",
|
|
137
|
+
gap: 6,
|
|
138
|
+
minHeight: 44,
|
|
139
|
+
paddingHorizontal: 12,
|
|
140
|
+
paddingVertical: 8,
|
|
141
|
+
},
|
|
142
|
+
strip.item(active),
|
|
143
|
+
item.disabled ? { opacity: 0.5 } : null,
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
item.icon
|
|
147
|
+
? React.createElement(Icon, { name: item.icon, size: 16, color: ink })
|
|
148
|
+
: null,
|
|
149
|
+
React.createElement(
|
|
150
|
+
Text,
|
|
151
|
+
{
|
|
152
|
+
numberOfLines: 1,
|
|
153
|
+
style: {
|
|
154
|
+
fontFamily: typography.fontFamily,
|
|
155
|
+
fontSize: typography.sizes.sm,
|
|
156
|
+
fontWeight: active ? "600" : "500",
|
|
157
|
+
color: ink,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
item.label,
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
Tabs.displayName = "Tabs";
|
|
168
|
+
return Tabs;
|
|
169
|
+
}
|
package/dist/tabs.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// `<Tabs>` (native). Binds ./tabs-view.js to React Native's own primitives;
|
|
2
|
+
// only the import differs from ./tabs.js.
|
|
3
|
+
|
|
4
|
+
import { View, Text, Pressable, ScrollView } from "react-native";
|
|
5
|
+
import { makeTabs } from "./tabs-view.js";
|
|
6
|
+
|
|
7
|
+
export const Tabs = makeTabs({ View, Text, Pressable, ScrollView });
|
|
@@ -65,6 +65,11 @@ function coerceToken(def, value) {
|
|
|
65
65
|
if (def.type === "gradient") {
|
|
66
66
|
return normaliseComponentGradient(value) || undefined;
|
|
67
67
|
}
|
|
68
|
+
if (def.type === "enum") {
|
|
69
|
+
return Array.isArray(def.values) && def.values.includes(value)
|
|
70
|
+
? value
|
|
71
|
+
: undefined;
|
|
72
|
+
}
|
|
68
73
|
return undefined;
|
|
69
74
|
}
|
|
70
75
|
|
package/dist/theme-components.js
CHANGED
|
@@ -57,6 +57,11 @@ function coerceToken(def, value) {
|
|
|
57
57
|
if (def.type === "gradient") {
|
|
58
58
|
return normaliseComponentGradient(value) || undefined;
|
|
59
59
|
}
|
|
60
|
+
if (def.type === "enum") {
|
|
61
|
+
return Array.isArray(def.values) && def.values.includes(value)
|
|
62
|
+
? value
|
|
63
|
+
: undefined;
|
|
64
|
+
}
|
|
60
65
|
return undefined;
|
|
61
66
|
}
|
|
62
67
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.163.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|