@colixsystems/widget-sdk 0.162.0 → 0.164.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/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
@@ -413,6 +416,7 @@ const MUTED_INPUT_FIELDS = Object.freeze({
413
416
  textColor: "inputTextColor",
414
417
  borderColor: "inputBorderColor",
415
418
  radius: "inputRadius",
419
+ height: "inputHeight",
416
420
  });
417
421
 
418
422
  // The Form widgets' choice-field option list -- the closest thing this
@@ -445,6 +449,16 @@ const LIST_ROW_FIELDS = Object.freeze({
445
449
  dividerWidth: "listDividerWidth",
446
450
  });
447
451
 
452
+ // The tab-strip vocabulary. Bound by name as well as onto User Management,
453
+ // whose Users / Groups / Invites strip is drawn by the SDK <Tabs>.
454
+ const TABS_FIELDS = Object.freeze({
455
+ style: "tabsStyle",
456
+ activeColor: "tabsActiveColor",
457
+ activeTextColor: "tabsActiveTextColor",
458
+ textColor: "tabsTextColor",
459
+ radius: "tabsRadius",
460
+ });
461
+
448
462
  const THEME_COMPONENTS = Object.freeze({
449
463
  button: Object.freeze({
450
464
  label: "Buttons",
@@ -563,6 +577,8 @@ const THEME_COMPONENTS = Object.freeze({
563
577
  textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurfaceMuted" }),
564
578
  borderColor: Object.freeze({ type: "color", uiDefault: "colors.border" }),
565
579
  radius: Object.freeze({ type: "size", min: 0, max: 48, uiDefault: "radii.sm" }),
580
+ // The field's minimum height; `<TextField>` reads it directly too.
581
+ height: Object.freeze({ type: "size", min: 32, max: 72 }),
566
582
  }),
567
583
  targets: Object.freeze({
568
584
  "appstudio.form-input": MUTED_INPUT_FIELDS,
@@ -614,6 +630,22 @@ const THEME_COMPONENTS = Object.freeze({
614
630
  "appstudio.data-list": LIST_ROW_FIELDS,
615
631
  }),
616
632
  }),
633
+ // Tab strips: `<Tabs>` and the page Tabs container read this scope directly;
634
+ // the fields bind it onto widgets. `style` is the first `enum` token.
635
+ tabs: Object.freeze({
636
+ label: "Tabs",
637
+ universalFields: TABS_FIELDS,
638
+ tokens: Object.freeze({
639
+ style: Object.freeze({ type: "enum", values: THEME_TAB_STYLES }),
640
+ activeColor: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
641
+ activeTextColor: Object.freeze({ type: "color" }),
642
+ textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurfaceMuted" }),
643
+ radius: Object.freeze({ type: "size", min: 0, max: 24 }),
644
+ }),
645
+ targets: Object.freeze({
646
+ "appstudio.user-management": TABS_FIELDS,
647
+ }),
648
+ }),
617
649
  });
618
650
 
619
651
  // sc-4505 — the value types a widget event payload field may declare. Narrower
@@ -2224,6 +2256,34 @@ const PRIMITIVES = [
2224
2256
  rnComponent: null,
2225
2257
  docsUrl: null,
2226
2258
  },
2259
+ {
2260
+ name: "Tabs",
2261
+ description:
2262
+ '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.',
2263
+ rnComponent: null,
2264
+ docsUrl: null,
2265
+ },
2266
+ {
2267
+ name: "Card",
2268
+ description:
2269
+ 'Themed card surface. `<Card>…</Card>` — THE way to draw a card, panel or tile: with no props it takes the app\'s card look (Theme Settings > Cards: background, border, corner radius, padding, shadow, gradient) so it matches every built-in card. Override one card with `background`, `borderColor`, `radius`, `padding` (a number or `{ top, right, bottom, left }`), `shadow` ("none" | "sm" | "md" | "lg") or `gradient` (`{ from, to, angle }`), or pass `style={props.style}` when your widget declares the `cardBackground` / `cardBorderColor` / `cardRadius` / `cardPadding` / `cardGradient` / `shadow` / `margin` style fields, so an author can restyle it. A gradient paints on both hosts. `as` renders the card as another container (e.g. `Pressable`, `ScrollView`) and every other prop lands on it. Never hand-roll a card from `View` + border + radius.',
2270
+ rnComponent: null,
2271
+ docsUrl: null,
2272
+ },
2273
+ {
2274
+ name: "Button",
2275
+ description:
2276
+ 'Themed button. `<Button title="Save" onPress={save} />` — THE way to draw a button: its fill, label colour, border, corner radius, padding, weight, shadow and gradient come from the app\'s button look (Theme Settings > Buttons, and Destructive / Focus ring for the danger variant and emphasis border), so it matches the built-in Button widget. Props: `title`, `onPress` (omit it for a purely visual button inside something else that is pressable), `variant` ("primary" default | "secondary" | "outlined" | "danger"), `icon` (a lucide name) with `iconPosition` ("leading" | "trailing"), `disabled`, `accessibilityLabel`, `testID`, `style` (extra container style, e.g. `alignSelf`) and `labelStyle`. Pass `fields` — `{ background, textColor, borderColor, borderWidth, paddingX, paddingY, radius, fontWeight, fontSize, shadow, gradient }` — to override one button, e.g. from style fields your widget declares. Never hand-roll a button from `Pressable` + background + radius.',
2277
+ rnComponent: null,
2278
+ docsUrl: null,
2279
+ },
2280
+ {
2281
+ name: "TextField",
2282
+ description:
2283
+ 'Themed text-entry field. `<TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />` — every `TextInput` prop works, and its background, text colour, border, corner radius and height come from the app\'s input look (Theme Settings > Muted), so a widget\'s fields match the built-in forms. `multiline` grows the field to a ceiling and then scrolls inside it. Override one field with `background`, `textColor`, `borderColor`, `radius` or `height`; `style` is merged last. Use it instead of styling a bare `TextInput`.',
2284
+ rnComponent: null,
2285
+ docsUrl: null,
2286
+ },
2227
2287
  ];
2228
2288
 
2229
2289
  const CATEGORIES = [
@@ -4413,7 +4473,17 @@ const CONTRACT = deepFreeze({
4413
4473
  // primitives that read it, plus `textRoleStyle` and its role helpers.
4414
4474
  // Widgets each hand-rolled their rows, so two lists in one app disagreed.
4415
4475
  // Additive: a theme without `list` paints the rows exactly as before.
4416
- version: "1.125.0",
4476
+ // 1.126.0: additive — the `tabs` component scope and the `Tabs` primitive
4477
+ // that reads it, and the `enum` component-token type (`values`), first
4478
+ // used by `tabs.style`. The page Tabs container renders through `Tabs` on
4479
+ // both hosts. Additive: an unthemed strip is the segmented control the
4480
+ // export always drew.
4481
+ // 1.127.0: additive — the `Card`, `Button` and `TextField` primitives,
4482
+ // lifted from the built-in widgets' shared card surface, button surface
4483
+ // and form field so every widget draws them from one implementation, plus
4484
+ // a `height` token on the `muted` scope. Additive: the built-ins render
4485
+ // exactly as before.
4486
+ version: "1.127.0",
4417
4487
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
4418
4488
  hooks: HOOKS,
4419
4489
  primitives: PRIMITIVES,
@@ -4426,6 +4496,7 @@ const CONTRACT = deepFreeze({
4426
4496
  themeTokens: DEFAULT_THEME_TOKENS,
4427
4497
  themeColorTokens: THEME_COLOR_TOKENS,
4428
4498
  themeComponents: THEME_COMPONENTS,
4499
+ themeComponentTabStyles: THEME_TAB_STYLES,
4429
4500
  themeComponentShadows: THEME_COMPONENT_SHADOWS,
4430
4501
  themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
4431
4502
  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
@@ -413,6 +416,7 @@ const MUTED_INPUT_FIELDS = Object.freeze({
413
416
  textColor: "inputTextColor",
414
417
  borderColor: "inputBorderColor",
415
418
  radius: "inputRadius",
419
+ height: "inputHeight",
416
420
  });
417
421
 
418
422
  // The Form widgets' choice-field option list -- the closest thing this
@@ -445,6 +449,16 @@ const LIST_ROW_FIELDS = Object.freeze({
445
449
  dividerWidth: "listDividerWidth",
446
450
  });
447
451
 
452
+ // The tab-strip vocabulary. Bound by name as well as onto User Management,
453
+ // whose Users / Groups / Invites strip is drawn by the SDK <Tabs>.
454
+ const TABS_FIELDS = Object.freeze({
455
+ style: "tabsStyle",
456
+ activeColor: "tabsActiveColor",
457
+ activeTextColor: "tabsActiveTextColor",
458
+ textColor: "tabsTextColor",
459
+ radius: "tabsRadius",
460
+ });
461
+
448
462
  const THEME_COMPONENTS = Object.freeze({
449
463
  button: Object.freeze({
450
464
  label: "Buttons",
@@ -563,6 +577,8 @@ const THEME_COMPONENTS = Object.freeze({
563
577
  textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurfaceMuted" }),
564
578
  borderColor: Object.freeze({ type: "color", uiDefault: "colors.border" }),
565
579
  radius: Object.freeze({ type: "size", min: 0, max: 48, uiDefault: "radii.sm" }),
580
+ // The field's minimum height; `<TextField>` reads it directly too.
581
+ height: Object.freeze({ type: "size", min: 32, max: 72 }),
566
582
  }),
567
583
  targets: Object.freeze({
568
584
  "appstudio.form-input": MUTED_INPUT_FIELDS,
@@ -614,6 +630,22 @@ const THEME_COMPONENTS = Object.freeze({
614
630
  "appstudio.data-list": LIST_ROW_FIELDS,
615
631
  }),
616
632
  }),
633
+ // Tab strips: `<Tabs>` and the page Tabs container read this scope directly;
634
+ // the fields bind it onto widgets. `style` is the first `enum` token.
635
+ tabs: Object.freeze({
636
+ label: "Tabs",
637
+ universalFields: TABS_FIELDS,
638
+ tokens: Object.freeze({
639
+ style: Object.freeze({ type: "enum", values: THEME_TAB_STYLES }),
640
+ activeColor: Object.freeze({ type: "color", uiDefault: "colors.primary" }),
641
+ activeTextColor: Object.freeze({ type: "color" }),
642
+ textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurfaceMuted" }),
643
+ radius: Object.freeze({ type: "size", min: 0, max: 24 }),
644
+ }),
645
+ targets: Object.freeze({
646
+ "appstudio.user-management": TABS_FIELDS,
647
+ }),
648
+ }),
617
649
  });
618
650
 
619
651
  // sc-4505 — the value types a widget event payload field may declare. Narrower
@@ -2224,6 +2256,34 @@ const PRIMITIVES = [
2224
2256
  rnComponent: null,
2225
2257
  docsUrl: null,
2226
2258
  },
2259
+ {
2260
+ name: "Tabs",
2261
+ description:
2262
+ '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.',
2263
+ rnComponent: null,
2264
+ docsUrl: null,
2265
+ },
2266
+ {
2267
+ name: "Card",
2268
+ description:
2269
+ 'Themed card surface. `<Card>…</Card>` — THE way to draw a card, panel or tile: with no props it takes the app\'s card look (Theme Settings > Cards: background, border, corner radius, padding, shadow, gradient) so it matches every built-in card. Override one card with `background`, `borderColor`, `radius`, `padding` (a number or `{ top, right, bottom, left }`), `shadow` ("none" | "sm" | "md" | "lg") or `gradient` (`{ from, to, angle }`), or pass `style={props.style}` when your widget declares the `cardBackground` / `cardBorderColor` / `cardRadius` / `cardPadding` / `cardGradient` / `shadow` / `margin` style fields, so an author can restyle it. A gradient paints on both hosts. `as` renders the card as another container (e.g. `Pressable`, `ScrollView`) and every other prop lands on it. Never hand-roll a card from `View` + border + radius.',
2270
+ rnComponent: null,
2271
+ docsUrl: null,
2272
+ },
2273
+ {
2274
+ name: "Button",
2275
+ description:
2276
+ 'Themed button. `<Button title="Save" onPress={save} />` — THE way to draw a button: its fill, label colour, border, corner radius, padding, weight, shadow and gradient come from the app\'s button look (Theme Settings > Buttons, and Destructive / Focus ring for the danger variant and emphasis border), so it matches the built-in Button widget. Props: `title`, `onPress` (omit it for a purely visual button inside something else that is pressable), `variant` ("primary" default | "secondary" | "outlined" | "danger"), `icon` (a lucide name) with `iconPosition` ("leading" | "trailing"), `disabled`, `accessibilityLabel`, `testID`, `style` (extra container style, e.g. `alignSelf`) and `labelStyle`. Pass `fields` — `{ background, textColor, borderColor, borderWidth, paddingX, paddingY, radius, fontWeight, fontSize, shadow, gradient }` — to override one button, e.g. from style fields your widget declares. Never hand-roll a button from `Pressable` + background + radius.',
2277
+ rnComponent: null,
2278
+ docsUrl: null,
2279
+ },
2280
+ {
2281
+ name: "TextField",
2282
+ description:
2283
+ 'Themed text-entry field. `<TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />` — every `TextInput` prop works, and its background, text colour, border, corner radius and height come from the app\'s input look (Theme Settings > Muted), so a widget\'s fields match the built-in forms. `multiline` grows the field to a ceiling and then scrolls inside it. Override one field with `background`, `textColor`, `borderColor`, `radius` or `height`; `style` is merged last. Use it instead of styling a bare `TextInput`.',
2284
+ rnComponent: null,
2285
+ docsUrl: null,
2286
+ },
2227
2287
  ];
2228
2288
 
2229
2289
  const CATEGORIES = [
@@ -4409,7 +4469,17 @@ const CONTRACT = deepFreeze({
4409
4469
  // primitives that read it, plus `textRoleStyle` and its role helpers.
4410
4470
  // Widgets each hand-rolled their rows, so two lists in one app disagreed.
4411
4471
  // Additive: a theme without `list` paints the rows exactly as before.
4412
- version: "1.125.0",
4472
+ // 1.126.0: additive — the `tabs` component scope and the `Tabs` primitive
4473
+ // that reads it, and the `enum` component-token type (`values`), first
4474
+ // used by `tabs.style`. The page Tabs container renders through `Tabs` on
4475
+ // both hosts. Additive: an unthemed strip is the segmented control the
4476
+ // export always drew.
4477
+ // 1.127.0: additive — the `Card`, `Button` and `TextField` primitives,
4478
+ // lifted from the built-in widgets' shared card surface, button surface
4479
+ // and form field so every widget draws them from one implementation, plus
4480
+ // a `height` token on the `muted` scope. Additive: the built-ins render
4481
+ // exactly as before.
4482
+ version: "1.127.0",
4413
4483
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
4414
4484
  hooks: HOOKS,
4415
4485
  primitives: PRIMITIVES,
@@ -4422,6 +4492,7 @@ const CONTRACT = deepFreeze({
4422
4492
  themeTokens: DEFAULT_THEME_TOKENS,
4423
4493
  themeColorTokens: THEME_COLOR_TOKENS,
4424
4494
  themeComponents: THEME_COMPONENTS,
4495
+ themeComponentTabStyles: THEME_TAB_STYLES,
4425
4496
  themeComponentShadows: THEME_COMPONENT_SHADOWS,
4426
4497
  themeComponentTextTransforms: THEME_COMPONENT_TEXT_TRANSFORMS,
4427
4498
  themeComponentGradient: THEME_COMPONENT_GRADIENT,
package/dist/icon.js CHANGED
@@ -18,10 +18,14 @@ import React from "react";
18
18
  import * as LucideIcons from "lucide-react-native";
19
19
  import { normalizeLucideIconName } from "./lucideIconName.js";
20
20
 
21
- export function Icon({ name, size, color }) {
21
+ /** The lucide component a name resolves to, or null for an unknown name. */
22
+ export function lucideGlyph(name) {
22
23
  const resolved = normalizeLucideIconName(name);
23
- const candidate = resolved ? LucideIcons[resolved] : null;
24
- const Component = candidate || LucideIcons.Square;
24
+ return (resolved && LucideIcons[resolved]) || null;
25
+ }
26
+
27
+ export function Icon({ name, size, color }) {
28
+ const Component = lucideGlyph(name) || LucideIcons.Square;
25
29
  const pixelSize = Number.isFinite(size) && size > 0 ? size : 24;
26
30
  return React.createElement(Component, {
27
31
  size: pixelSize,
package/dist/index.d.ts CHANGED
@@ -2615,6 +2615,118 @@ export const EmptyState: (props: {
2615
2615
  children?: ReactNode;
2616
2616
  }) => any;
2617
2617
 
2618
+ /**
2619
+ * Themed card surface. With no props it takes the app's card look (Theme
2620
+ * Settings > Cards). Props override one card; `style` takes the `card*` style
2621
+ * fields a widget declares; `as` renders it as another container.
2622
+ *
2623
+ * @example
2624
+ * <Card><Text>…</Text></Card>
2625
+ * <Card as={Pressable} onPress={open} radius={12} shadow="md">…</Card>
2626
+ */
2627
+ export const Card: (props: {
2628
+ background?: string;
2629
+ borderColor?: string;
2630
+ radius?: number | { topLeft?: number; topRight?: number; bottomRight?: number; bottomLeft?: number };
2631
+ padding?: number | { top?: number; right?: number; bottom?: number; left?: number };
2632
+ shadow?: "none" | "sm" | "md" | "lg";
2633
+ gradient?: { from: string; to: string; angle?: number };
2634
+ /** `card*` style fields, `shadow`, `margin` — a widget's per-instance style. */
2635
+ style?: Record<string, any>;
2636
+ /** The caller's own base style; a card given none starts from the theme. */
2637
+ baseStyle?: any;
2638
+ as?: any;
2639
+ fillInGradient?: boolean;
2640
+ containerStyle?: any;
2641
+ withoutMargin?: boolean;
2642
+ children?: ReactNode;
2643
+ [prop: string]: any;
2644
+ }) => any;
2645
+
2646
+ export type ButtonVariant = "primary" | "secondary" | "outlined" | "danger";
2647
+ export const BUTTON_VARIANTS: readonly ButtonVariant[];
2648
+
2649
+ /**
2650
+ * Themed button (Theme Settings > Buttons, Destructive, Focus ring). Omit
2651
+ * `onPress` for a purely visual button inside something pressable.
2652
+ *
2653
+ * @example
2654
+ * <Button title="Save" onPress={save} />
2655
+ * <Button title="Delete" variant="danger" icon="trash-2" onPress={remove} />
2656
+ */
2657
+ export const Button: (props: {
2658
+ title: ReactNode;
2659
+ variant?: ButtonVariant;
2660
+ icon?: string;
2661
+ iconPosition?: "leading" | "trailing";
2662
+ onPress?: () => void;
2663
+ disabled?: boolean;
2664
+ /** Per-button surface fields; unset ones follow the theme. */
2665
+ fields?: {
2666
+ background?: string;
2667
+ textColor?: string;
2668
+ borderColor?: string;
2669
+ borderWidth?: number;
2670
+ paddingX?: number;
2671
+ paddingY?: number;
2672
+ radius?: number;
2673
+ fontWeight?: string;
2674
+ fontSize?: number;
2675
+ shadow?: "none" | "sm" | "md" | "lg";
2676
+ gradient?: { from: string; to: string; angle?: number };
2677
+ };
2678
+ style?: any;
2679
+ labelStyle?: any;
2680
+ accessibilityLabel?: string;
2681
+ testID?: string;
2682
+ }) => any;
2683
+
2684
+ /**
2685
+ * Themed text-entry field (Theme Settings > Muted). Every TextInput prop works.
2686
+ *
2687
+ * @example
2688
+ * <TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />
2689
+ */
2690
+ export const TextField: (props: {
2691
+ background?: string;
2692
+ textColor?: string;
2693
+ borderColor?: string;
2694
+ radius?: number;
2695
+ height?: number;
2696
+ multiline?: boolean;
2697
+ style?: any;
2698
+ [prop: string]: any;
2699
+ }) => any;
2700
+
2701
+ export type TabStyle = "segmented" | "pill" | "underline";
2702
+
2703
+ /**
2704
+ * Themed tab strip. Its look (segmented, pills or underline), colours and
2705
+ * corners come from the theme's `tabs` scope, so every strip in the app —
2706
+ * including the page Tabs container — matches. Draws the strip only; render
2707
+ * the selected panel yourself.
2708
+ *
2709
+ * @example
2710
+ * const [tab, setTab] = useState("open");
2711
+ * <Tabs items={[{ key: "open", label: "Open" }, { key: "done", label: "Done" }]} value={tab} onChange={setTab} />
2712
+ */
2713
+ export const Tabs: (props: {
2714
+ items: Array<{ key: string; label: ReactNode; icon?: string; disabled?: boolean; testID?: string }>;
2715
+ value?: string;
2716
+ onChange?: (key: string) => void;
2717
+ /** Overrides the theme's tab style for this strip. */
2718
+ variant?: TabStyle;
2719
+ activeColor?: string;
2720
+ activeTextColor?: string;
2721
+ textColor?: string;
2722
+ radius?: number;
2723
+ /** For hosts rendering outside a widget context; widgets omit it. */
2724
+ theme?: any;
2725
+ accessibilityLabel?: string;
2726
+ style?: any;
2727
+ testID?: string;
2728
+ }) => any;
2729
+
2618
2730
  export type TextRole = "display" | "title" | "subtitle" | "body" | "caption" | "overline";
2619
2731
  export const TEXT_ROLES: readonly TextRole[];
2620
2732
  export const TEXT_TRANSFORMS: readonly string[];
package/dist/index.js CHANGED
@@ -191,6 +191,36 @@ 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
+ // The one card surface: the theme's `card` scope, a gradient fill, margin.
195
+ export { Card } from "./card.js";
196
+ export {
197
+ SHADOW_PRESETS,
198
+ shadowStyle,
199
+ SPACING_MAX_PX,
200
+ CARD_RADIUS_MAX_PX,
201
+ FRAME_RADIUS_MAX_PX,
202
+ marginStyle,
203
+ spacingOverlay,
204
+ cornerRadiusOverlay,
205
+ cardSurfaceStyle,
206
+ cardOverlay,
207
+ gradientProps,
208
+ cardGradientProps,
209
+ } from "./card-style.js";
210
+ // The themed button; follows the theme's button, destructive and ring scopes.
211
+ export { Button, BUTTON_VARIANTS } from "./button.js";
212
+ export {
213
+ BUTTON_PADDING_MAX,
214
+ BUTTON_BORDER_WIDTH_MAX,
215
+ SELF_ALIGNMENTS,
216
+ buttonSurfaceOverlay,
217
+ buttonSurfaceTextOverlay,
218
+ selfAlignStyle,
219
+ } from "./button-style.js";
220
+ // The themed text-entry field; follows the theme's `muted` scope.
221
+ export { TextField } from "./text-field.js";
222
+ // A themed tab strip; follows the theme's `tabs` scope.
223
+ export { Tabs } from "./tabs.js";
194
224
  // The semantic text tiers, shared with the built-in widgets.
195
225
  export {
196
226
  textRoleStyle,
@@ -184,6 +184,36 @@ 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
+ // The one card surface: the theme's `card` scope, a gradient fill, margin.
188
+ export { Card } from "./card.native.js";
189
+ export {
190
+ SHADOW_PRESETS,
191
+ shadowStyle,
192
+ SPACING_MAX_PX,
193
+ CARD_RADIUS_MAX_PX,
194
+ FRAME_RADIUS_MAX_PX,
195
+ marginStyle,
196
+ spacingOverlay,
197
+ cornerRadiusOverlay,
198
+ cardSurfaceStyle,
199
+ cardOverlay,
200
+ gradientProps,
201
+ cardGradientProps,
202
+ } from "./card-style.js";
203
+ // The themed button; follows the theme's button, destructive and ring scopes.
204
+ export { Button, BUTTON_VARIANTS } from "./button.native.js";
205
+ export {
206
+ BUTTON_PADDING_MAX,
207
+ BUTTON_BORDER_WIDTH_MAX,
208
+ SELF_ALIGNMENTS,
209
+ buttonSurfaceOverlay,
210
+ buttonSurfaceTextOverlay,
211
+ selfAlignStyle,
212
+ } from "./button-style.js";
213
+ // The themed text-entry field; follows the theme's `muted` scope.
214
+ export { TextField } from "./text-field.native.js";
215
+ // A themed tab strip; follows the theme's `tabs` scope.
216
+ export { Tabs } from "./tabs.native.js";
187
217
  // The semantic text tiers, shared with the built-in widgets.
188
218
  export {
189
219
  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
+ }