@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 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.162.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**.
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
- version: "1.125.0",
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
- version: "1.125.0",
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,
@@ -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>` (web). Binds ./tabs-view.js to react-native-web; see
2
+ // ./primitives.js for why not the bare specifier.
3
+
4
+ import * as ReactNative from "react-native-web";
5
+ import { makeTabs } from "./tabs-view.js";
6
+
7
+ export const Tabs = makeTabs(ReactNative);
@@ -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
 
@@ -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.162.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",