@rific/auto-paper 0.9.2 → 0.9.3

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
@@ -327,6 +327,27 @@ function SettingsScreen() {
327
327
  | `showLabels` | `boolean` | Set `false` for icon-only segments, useful in tight layouts where "System" would truncate. `accessibilityLabel` is always set regardless, so screen readers still announce the full word. Defaults to `true` |
328
328
  | `icons` | `AppearanceIcons` | Per-value icon overrides: pass just the ones you want to change (e.g. `{ system: 'theme-light-dark' }`), merged over the defaults (`monitor` / `white-balance-sunny` / `weather-night`) |
329
329
 
330
+ ### `HarmonyPicker`
331
+
332
+ Switches between the 6 `ColorHarmony` modes (`triadic`, `split-complementary`, `analogous`, `square`, `complementary`, `double-split`) via `react-native-paper`'s `SegmentedButtons`. `PalettePicker` can embed this same picker in its own dialog instead of standing alone — see `onHarmonyChange` below.
333
+
334
+ ```tsx
335
+ import { HarmonyPicker, useThemeSettings } from '@rific/auto-paper'
336
+
337
+ function SettingsScreen() {
338
+ const { settings: { harmony }, set } = useThemeSettings()
339
+ return <HarmonyPicker value={harmony} onChange={(h) => set({ harmony: h })} showLabels={false} />
340
+ }
341
+ ```
342
+
343
+ | Prop | Type | Description |
344
+ |---|---|---|
345
+ | `value` | `ColorHarmony` | Currently selected harmony |
346
+ | `onChange` | `(harmony: ColorHarmony) => void` | Called when a segment is tapped |
347
+ | `showLabels` | `boolean` | Set `false` for icon-only segments. Also lowers Paper's default 76px-per-segment minimum width down to 0, so all 6 segments can always shrink to fit — however narrow the container, they resize down together instead of overflowing it. `accessibilityLabel` is always set regardless, so screen readers still announce the full harmony name either way. Defaults to `true` |
348
+ | `checkedColor` / `uncheckedColor` | `string` | Icon color for the checked and unchecked segments, matching Paper's own segmented-button vocabulary |
349
+ | `checkedContainerColor` / `uncheckedContainerColor` | `string` | Fill color for the checked and unchecked segments. Omit any of these four color props to fall back to Paper's default segmented-button styling |
350
+
330
351
  ### `Appbar`
331
352
 
332
353
  A thin wrapper around `react-native-paper`'s `Appbar` that automatically syncs `StatusBar` background color and bar style to the current theme surface color.
@@ -432,14 +453,14 @@ import { IconButton } from '@rific/auto-paper'
432
453
 
433
454
  ### `PalettePicker`
434
455
 
435
- Like `ColorPicker`, but each swatch (and the trigger itself) renders the seed's full triadic palette as a 3-wedge pie instead of a flat color, so you see the whole result before picking.
456
+ Like `ColorPicker`, but each swatch (and the trigger itself) renders the seed's full triadic palette as a 3-wedge pie instead of a flat color, so you see the whole result before picking. Pass `onHarmonyChange` to fold a compact, icon-only `HarmonyPicker` row into the same dialog, above the swatch grid — every swatch's own pie reshapes live as harmony changes, so there's one place to tune both instead of two separate controls. Omit it and this renders exactly as a plain color-only picker always has.
436
457
 
437
458
  ```tsx
438
459
  import { PalettePicker, useThemeSettings } from '@rific/auto-paper'
439
460
 
440
461
  function SettingsScreen() {
441
462
  const { settings: { color, harmony }, set } = useThemeSettings()
442
- return <PalettePicker value={color} harmony={harmony} onChange={(c) => set({ color: c })} />
463
+ return <PalettePicker value={color} harmony={harmony} onChange={(c) => set({ color: c })} onHarmonyChange={(h) => set({ harmony: h })} />
443
464
  }
444
465
  ```
445
466
 
@@ -448,6 +469,7 @@ function SettingsScreen() {
448
469
  | `value` | `string` | Currently selected seed color |
449
470
  | `onChange` | `(color: string) => void` | Called with the seed color when a swatch is tapped |
450
471
  | `harmony` | `ColorHarmony` | Harmony used to compute each swatch's preview palette. Defaults to `'split-complementary'` |
472
+ | `onHarmonyChange` | `(harmony: ColorHarmony) => void` | Opt-in: passing this renders the `HarmonyPicker` row inside the dialog. Gated on the callback itself rather than a separate boolean, so there's no way to pass one without the other |
451
473
  | `weights` | `PaletteWeights` | Relative size of each wedge: `{ primary, secondary, tertiary }`. Defaults to `{ primary: 2, secondary: 1, tertiary: 1 }` (primary half the pie, secondary/tertiary a quarter each). Values are normalized, so any positive ratio works; keep each share at or under half the total so its wedge stays a single slice |
452
474
  | `colors` | `SeedColor[]` | Seed color swatches to offer. Defaults to the same set as `ColorPicker` |
453
475
  | `blur` | `boolean` | Overrides the ambient blur setting for this component's dialog |
package/dist/index.d.mts CHANGED
@@ -153,9 +153,13 @@ declare function FAB(props: FABProps): react.JSX.Element;
153
153
  type Props$1 = {
154
154
  value: ColorHarmony;
155
155
  onChange: (harmony: ColorHarmony) => void;
156
- blur?: boolean;
156
+ showLabels?: boolean;
157
+ checkedColor?: string;
158
+ uncheckedColor?: string;
159
+ checkedContainerColor?: string;
160
+ uncheckedContainerColor?: string;
157
161
  };
158
- declare const HarmonyPicker: ({ value, onChange, blur: blurProp }: Props$1) => react.JSX.Element;
162
+ declare const HarmonyPicker: ({ value, onChange, showLabels, checkedColor, uncheckedColor, checkedContainerColor, uncheckedContainerColor }: Props$1) => react.JSX.Element;
159
163
 
160
164
  type IconButtonProps = IconButtonProps$1 & {
161
165
  variant?: 'primary' | 'secondary' | 'tertiary' | 'surface';
@@ -178,11 +182,12 @@ type Props = {
178
182
  value: string;
179
183
  onChange: (color: string) => void;
180
184
  harmony?: ColorHarmony;
185
+ onHarmonyChange?: (harmony: ColorHarmony) => void;
181
186
  weights?: PaletteWeights;
182
187
  colors?: SeedColor[];
183
188
  blur?: boolean;
184
189
  };
185
- declare const PalettePicker: ({ value, onChange, harmony, weights, colors, blur: blurProp }: Props) => react.JSX.Element;
190
+ declare const PalettePicker: ({ value, onChange, harmony, onHarmonyChange, weights, colors, blur: blurProp }: Props) => react.JSX.Element;
186
191
 
187
192
  declare const TextInput: react.ForwardRefExoticComponent<Omit<TextInputProps, "ref"> & react.RefAttributes<TextInput$1>>;
188
193
 
package/dist/index.d.ts CHANGED
@@ -153,9 +153,13 @@ declare function FAB(props: FABProps): react.JSX.Element;
153
153
  type Props$1 = {
154
154
  value: ColorHarmony;
155
155
  onChange: (harmony: ColorHarmony) => void;
156
- blur?: boolean;
156
+ showLabels?: boolean;
157
+ checkedColor?: string;
158
+ uncheckedColor?: string;
159
+ checkedContainerColor?: string;
160
+ uncheckedContainerColor?: string;
157
161
  };
158
- declare const HarmonyPicker: ({ value, onChange, blur: blurProp }: Props$1) => react.JSX.Element;
162
+ declare const HarmonyPicker: ({ value, onChange, showLabels, checkedColor, uncheckedColor, checkedContainerColor, uncheckedContainerColor }: Props$1) => react.JSX.Element;
159
163
 
160
164
  type IconButtonProps = IconButtonProps$1 & {
161
165
  variant?: 'primary' | 'secondary' | 'tertiary' | 'surface';
@@ -178,11 +182,12 @@ type Props = {
178
182
  value: string;
179
183
  onChange: (color: string) => void;
180
184
  harmony?: ColorHarmony;
185
+ onHarmonyChange?: (harmony: ColorHarmony) => void;
181
186
  weights?: PaletteWeights;
182
187
  colors?: SeedColor[];
183
188
  blur?: boolean;
184
189
  };
185
- declare const PalettePicker: ({ value, onChange, harmony, weights, colors, blur: blurProp }: Props) => react.JSX.Element;
190
+ declare const PalettePicker: ({ value, onChange, harmony, onHarmonyChange, weights, colors, blur: blurProp }: Props) => react.JSX.Element;
186
191
 
187
192
  declare const TextInput: react.ForwardRefExoticComponent<Omit<TextInputProps, "ref"> & react.RefAttributes<TextInput$1>>;
188
193
 
package/dist/index.js CHANGED
@@ -899,26 +899,8 @@ function FAB(props) {
899
899
  }
900
900
 
901
901
  // src/components/HarmonyPicker.tsx
902
- var import_react11 = require("react");
903
- var import_react_native_paper13 = require("react-native-paper");
904
-
905
- // src/components/Menu.tsx
906
- var import_react_native8 = require("react-native");
907
902
  var import_react_native_paper12 = require("react-native-paper");
908
903
  var import_jsx_runtime11 = require("react/jsx-runtime");
909
- var MenuComponent = ({ blur: blurProp, children, ...props }) => {
910
- const blur = useBlur(blurProp);
911
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native_paper12.Menu, { ...props, contentStyle: [styles4.content, props.contentStyle], children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(BlurView, { blur, children }) });
912
- };
913
- var Menu = Object.assign(MenuComponent, {
914
- Item: import_react_native_paper12.Menu.Item
915
- });
916
- var styles4 = import_react_native8.StyleSheet.create({
917
- content: { backgroundColor: "transparent", paddingVertical: 0 }
918
- });
919
-
920
- // src/components/HarmonyPicker.tsx
921
- var import_jsx_runtime12 = require("react/jsx-runtime");
922
904
  var HARMONIES = [
923
905
  { label: "Triadic", value: "triadic", icon: "triangle-outline" },
924
906
  { label: "Split", value: "split-complementary", icon: "arrow-split-vertical" },
@@ -927,46 +909,56 @@ var HARMONIES = [
927
909
  { label: "Complement", value: "complementary", icon: "circle-half-full" },
928
910
  { label: "Dbl Split", value: "double-split", icon: "arrow-expand-horizontal" }
929
911
  ];
930
- var HarmonyPicker = ({ value, onChange, blur: blurProp }) => {
931
- const blur = useBlur(blurProp);
932
- const [open, setOpen] = (0, import_react11.useState)(false);
933
- const selected = HARMONIES.find((h) => h.value === value);
934
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
935
- Menu,
936
- {
937
- blur,
938
- visible: open,
939
- onDismiss: () => setOpen(false),
940
- anchor: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native_paper13.Button, { mode: "outlined", icon: selected.icon, onPress: () => setOpen(true), children: selected.label }),
941
- children: HARMONIES.map((h) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
942
- Menu.Item,
943
- {
944
- leadingIcon: h.value === value ? "check" : h.icon,
945
- title: h.label,
946
- onPress: () => {
947
- onChange(h.value);
948
- setOpen(false);
949
- }
950
- },
951
- h.value
952
- ))
953
- }
954
- );
955
- };
912
+ var COMPACT_MIN_WIDTH = 0;
913
+ var HarmonyPicker = ({ value, onChange, showLabels = true, checkedColor, uncheckedColor, checkedContainerColor, uncheckedContainerColor }) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
914
+ import_react_native_paper12.SegmentedButtons,
915
+ {
916
+ value,
917
+ onValueChange: (v) => onChange(v),
918
+ buttons: HARMONIES.map((h) => {
919
+ const checked = h.value === value;
920
+ const containerColor = checked ? checkedContainerColor : uncheckedContainerColor;
921
+ return {
922
+ value: h.value,
923
+ icon: h.icon,
924
+ label: showLabels ? h.label : void 0,
925
+ accessibilityLabel: h.label,
926
+ checkedColor,
927
+ uncheckedColor,
928
+ style: [showLabels ? void 0 : { minWidth: COMPACT_MIN_WIDTH }, containerColor ? { backgroundColor: containerColor } : void 0]
929
+ };
930
+ })
931
+ }
932
+ );
956
933
 
957
934
  // src/components/IconButton.tsx
958
- var import_react_native_paper14 = require("react-native-paper");
959
- var import_jsx_runtime13 = require("react/jsx-runtime");
935
+ var import_react_native_paper13 = require("react-native-paper");
936
+ var import_jsx_runtime12 = require("react/jsx-runtime");
960
937
  function IconButton({ variant, iconColor, containerColor, ...props }) {
961
938
  const defaults = usePaperDefaults();
962
- const { colors } = (0, import_react_native_paper14.useTheme)();
939
+ const { colors } = (0, import_react_native_paper13.useTheme)();
963
940
  const variantContainerColor = variant === "primary" ? colors.primaryContainer : variant === "secondary" ? colors.secondaryContainer : variant === "tertiary" ? colors.tertiaryContainer : variant === "surface" ? colors.surface : void 0;
964
941
  const variantIconColor = variant === "primary" ? colors.onPrimaryContainer : variant === "secondary" ? colors.onSecondaryContainer : variant === "tertiary" ? colors.onTertiaryContainer : variant === "surface" ? colors.onSurface : void 0;
965
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native_paper14.IconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
942
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native_paper13.IconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
966
943
  }
967
944
 
945
+ // src/components/Menu.tsx
946
+ var import_react_native8 = require("react-native");
947
+ var import_react_native_paper14 = require("react-native-paper");
948
+ var import_jsx_runtime13 = require("react/jsx-runtime");
949
+ var MenuComponent = ({ blur: blurProp, children, ...props }) => {
950
+ const blur = useBlur(blurProp);
951
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native_paper14.Menu, { ...props, contentStyle: [styles4.content, props.contentStyle], children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(BlurView, { blur, children }) });
952
+ };
953
+ var Menu = Object.assign(MenuComponent, {
954
+ Item: import_react_native_paper14.Menu.Item
955
+ });
956
+ var styles4 = import_react_native8.StyleSheet.create({
957
+ content: { backgroundColor: "transparent", paddingVertical: 0 }
958
+ });
959
+
968
960
  // src/components/PalettePicker.tsx
969
- var import_react12 = require("react");
961
+ var import_react11 = require("react");
970
962
  var import_react_native9 = require("react-native");
971
963
  var import_react_native_paper15 = require("react-native-paper");
972
964
  var import_jsx_runtime14 = require("react/jsx-runtime");
@@ -984,35 +976,39 @@ var buildWedges = (weights) => {
984
976
  ];
985
977
  };
986
978
  var Pie = ({ palette, weights = defaultWeights, size = SIZE2 }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: [styles5.pie, { borderRadius: size / 2, height: size, width: size }], children: buildWedges(weights).map(({ key, rotate, share }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: [styles5.rotator, { transform: [{ rotate: `${rotate}deg` }] }], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: styles5.halfMask, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: [styles5.innerRotator, { transform: [{ rotate: `${share - 180}deg` }] }], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: [styles5.wedgeFill, { backgroundColor: palette[key] }] }) }) }) }, key)) });
987
- var PalettePicker = ({ value, onChange, harmony = "split-complementary", weights, colors = defaultColors, blur: blurProp }) => {
979
+ var PalettePicker = ({ value, onChange, harmony = "split-complementary", onHarmonyChange, weights, colors = defaultColors, blur: blurProp }) => {
988
980
  const blur = useBlur(blurProp);
989
981
  const { colors: themeColors } = (0, import_react_native_paper15.useTheme)();
990
- const [open, setOpen] = (0, import_react12.useState)(false);
982
+ const [open, setOpen] = (0, import_react11.useState)(false);
991
983
  const { onLayout, size } = useSwatchSize(SIZE2);
992
984
  return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
993
985
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper15.TouchableRipple, { onPress: () => setOpen(true), style: styles5.trigger, borderless: true, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native9.View, { style: styles5.triggerContent, children: [
994
986
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Pie, { palette: getTriadicPalette(value, harmony), weights }),
995
987
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { pointerEvents: "none", style: [styles5.triggerIcon, { borderRadius: SIZE2 / 2 }, getSwatchRing(value, false, themeColors.outlineVariant)], children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper15.Icon, { source: "palette", size: 20, color: getContrastColor(value) }) })
996
988
  ] }) }),
997
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Dialog.Content, { children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: styles5.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
998
- import_react_native9.TouchableOpacity,
999
- {
1000
- onPress: () => {
1001
- onChange(hex);
1002
- setOpen(false);
989
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(Dialog.Content, { children: [
990
+ onHarmonyChange && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: styles5.harmonyRow, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(HarmonyPicker, { value: harmony, onChange: onHarmonyChange, showLabels: false }) }),
991
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { style: styles5.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
992
+ import_react_native9.TouchableOpacity,
993
+ {
994
+ onPress: () => {
995
+ onChange(hex);
996
+ setOpen(false);
997
+ },
998
+ style: [styles5.swatch, { height: size, width: size }],
999
+ children: [
1000
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Pie, { palette: getTriadicPalette(hex, harmony), weights, size }),
1001
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { pointerEvents: "none", style: [styles5.ring, { borderRadius: size / 2 }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)], children: value === hex && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper15.Icon, { source: "check", size: 20, color: getContrastColor(hex) }) })
1002
+ ]
1003
1003
  },
1004
- style: [styles5.swatch, { height: size, width: size }],
1005
- children: [
1006
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Pie, { palette: getTriadicPalette(hex, harmony), weights, size }),
1007
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native9.View, { pointerEvents: "none", style: [styles5.ring, { borderRadius: size / 2 }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)], children: value === hex && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper15.Icon, { source: "check", size: 20, color: getContrastColor(hex) }) })
1008
- ]
1009
- },
1010
- hex
1011
- )) }) }) })
1004
+ hex
1005
+ )) })
1006
+ ] }) })
1012
1007
  ] });
1013
1008
  };
1014
1009
  var styles5 = import_react_native9.StyleSheet.create({
1015
1010
  halfMask: { height: "100%", left: "50%", overflow: "hidden", position: "absolute", top: 0, width: "50%" },
1011
+ harmonyRow: { marginBottom: 4 },
1016
1012
  innerRotator: { height: "100%", left: "-100%", overflow: "hidden", position: "absolute", top: 0, width: "200%" },
1017
1013
  pie: { overflow: "hidden" },
1018
1014
  rotator: { height: "100%", left: 0, overflow: "hidden", position: "absolute", top: 0, width: "100%" },
@@ -1026,10 +1022,10 @@ var styles5 = import_react_native9.StyleSheet.create({
1026
1022
  });
1027
1023
 
1028
1024
  // src/components/TextInput.tsx
1029
- var import_react13 = require("react");
1025
+ var import_react12 = require("react");
1030
1026
  var import_react_native_paper16 = require("react-native-paper");
1031
1027
  var import_jsx_runtime15 = require("react/jsx-runtime");
1032
- var TextInput = (0, import_react13.forwardRef)(function TextInput2(props, ref) {
1028
+ var TextInput = (0, import_react12.forwardRef)(function TextInput2(props, ref) {
1033
1029
  const defaults = usePaperDefaults();
1034
1030
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native_paper16.TextInput, { ref, ...defaults.TextInput, ...props });
1035
1031
  });
@@ -1076,8 +1072,8 @@ var import_react_native_paper17 = require("react-native-paper");
1076
1072
  var useAutoPaperTheme = () => (0, import_react_native_paper17.useTheme)();
1077
1073
 
1078
1074
  // src/useThemeSettings.ts
1079
- var import_react14 = require("react");
1080
- var useThemeSettings = () => (0, import_react14.useContext)(ThemeSettingsContext);
1075
+ var import_react13 = require("react");
1076
+ var useThemeSettings = () => (0, import_react13.useContext)(ThemeSettingsContext);
1081
1077
 
1082
1078
  // src/utils/getThirdColor.ts
1083
1079
  function rgbToHsl(r, g, b) {
package/dist/index.mjs CHANGED
@@ -820,26 +820,8 @@ function FAB(props) {
820
820
  }
821
821
 
822
822
  // src/components/HarmonyPicker.tsx
823
- import { useState as useState6 } from "react";
824
- import { Button as Button2 } from "react-native-paper";
825
-
826
- // src/components/Menu.tsx
827
- import { StyleSheet as StyleSheet5 } from "react-native";
828
- import { Menu as PaperMenu } from "react-native-paper";
823
+ import { SegmentedButtons as SegmentedButtons2 } from "react-native-paper";
829
824
  import { jsx as jsx11 } from "react/jsx-runtime";
830
- var MenuComponent = ({ blur: blurProp, children, ...props }) => {
831
- const blur = useBlur(blurProp);
832
- return /* @__PURE__ */ jsx11(PaperMenu, { ...props, contentStyle: [styles4.content, props.contentStyle], children: /* @__PURE__ */ jsx11(BlurView, { blur, children }) });
833
- };
834
- var Menu = Object.assign(MenuComponent, {
835
- Item: PaperMenu.Item
836
- });
837
- var styles4 = StyleSheet5.create({
838
- content: { backgroundColor: "transparent", paddingVertical: 0 }
839
- });
840
-
841
- // src/components/HarmonyPicker.tsx
842
- import { jsx as jsx12 } from "react/jsx-runtime";
843
825
  var HARMONIES = [
844
826
  { label: "Triadic", value: "triadic", icon: "triangle-outline" },
845
827
  { label: "Split", value: "split-complementary", icon: "arrow-split-vertical" },
@@ -848,46 +830,56 @@ var HARMONIES = [
848
830
  { label: "Complement", value: "complementary", icon: "circle-half-full" },
849
831
  { label: "Dbl Split", value: "double-split", icon: "arrow-expand-horizontal" }
850
832
  ];
851
- var HarmonyPicker = ({ value, onChange, blur: blurProp }) => {
852
- const blur = useBlur(blurProp);
853
- const [open, setOpen] = useState6(false);
854
- const selected = HARMONIES.find((h) => h.value === value);
855
- return /* @__PURE__ */ jsx12(
856
- Menu,
857
- {
858
- blur,
859
- visible: open,
860
- onDismiss: () => setOpen(false),
861
- anchor: /* @__PURE__ */ jsx12(Button2, { mode: "outlined", icon: selected.icon, onPress: () => setOpen(true), children: selected.label }),
862
- children: HARMONIES.map((h) => /* @__PURE__ */ jsx12(
863
- Menu.Item,
864
- {
865
- leadingIcon: h.value === value ? "check" : h.icon,
866
- title: h.label,
867
- onPress: () => {
868
- onChange(h.value);
869
- setOpen(false);
870
- }
871
- },
872
- h.value
873
- ))
874
- }
875
- );
876
- };
833
+ var COMPACT_MIN_WIDTH = 0;
834
+ var HarmonyPicker = ({ value, onChange, showLabels = true, checkedColor, uncheckedColor, checkedContainerColor, uncheckedContainerColor }) => /* @__PURE__ */ jsx11(
835
+ SegmentedButtons2,
836
+ {
837
+ value,
838
+ onValueChange: (v) => onChange(v),
839
+ buttons: HARMONIES.map((h) => {
840
+ const checked = h.value === value;
841
+ const containerColor = checked ? checkedContainerColor : uncheckedContainerColor;
842
+ return {
843
+ value: h.value,
844
+ icon: h.icon,
845
+ label: showLabels ? h.label : void 0,
846
+ accessibilityLabel: h.label,
847
+ checkedColor,
848
+ uncheckedColor,
849
+ style: [showLabels ? void 0 : { minWidth: COMPACT_MIN_WIDTH }, containerColor ? { backgroundColor: containerColor } : void 0]
850
+ };
851
+ })
852
+ }
853
+ );
877
854
 
878
855
  // src/components/IconButton.tsx
879
856
  import { IconButton as PaperIconButton, useTheme as useTheme7 } from "react-native-paper";
880
- import { jsx as jsx13 } from "react/jsx-runtime";
857
+ import { jsx as jsx12 } from "react/jsx-runtime";
881
858
  function IconButton({ variant, iconColor, containerColor, ...props }) {
882
859
  const defaults = usePaperDefaults();
883
860
  const { colors } = useTheme7();
884
861
  const variantContainerColor = variant === "primary" ? colors.primaryContainer : variant === "secondary" ? colors.secondaryContainer : variant === "tertiary" ? colors.tertiaryContainer : variant === "surface" ? colors.surface : void 0;
885
862
  const variantIconColor = variant === "primary" ? colors.onPrimaryContainer : variant === "secondary" ? colors.onSecondaryContainer : variant === "tertiary" ? colors.onTertiaryContainer : variant === "surface" ? colors.onSurface : void 0;
886
- return /* @__PURE__ */ jsx13(PaperIconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
863
+ return /* @__PURE__ */ jsx12(PaperIconButton, { ...defaults.IconButton, containerColor: containerColor ?? variantContainerColor, iconColor: iconColor ?? variantIconColor, ...props });
887
864
  }
888
865
 
866
+ // src/components/Menu.tsx
867
+ import { StyleSheet as StyleSheet5 } from "react-native";
868
+ import { Menu as PaperMenu } from "react-native-paper";
869
+ import { jsx as jsx13 } from "react/jsx-runtime";
870
+ var MenuComponent = ({ blur: blurProp, children, ...props }) => {
871
+ const blur = useBlur(blurProp);
872
+ return /* @__PURE__ */ jsx13(PaperMenu, { ...props, contentStyle: [styles4.content, props.contentStyle], children: /* @__PURE__ */ jsx13(BlurView, { blur, children }) });
873
+ };
874
+ var Menu = Object.assign(MenuComponent, {
875
+ Item: PaperMenu.Item
876
+ });
877
+ var styles4 = StyleSheet5.create({
878
+ content: { backgroundColor: "transparent", paddingVertical: 0 }
879
+ });
880
+
889
881
  // src/components/PalettePicker.tsx
890
- import { useState as useState7 } from "react";
882
+ import { useState as useState6 } from "react";
891
883
  import { StyleSheet as StyleSheet6, TouchableOpacity as TouchableOpacity2, View as View5 } from "react-native";
892
884
  import { Icon as Icon2, TouchableRipple as TouchableRipple2, useTheme as useTheme8 } from "react-native-paper";
893
885
  import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs6 } from "react/jsx-runtime";
@@ -905,35 +897,39 @@ var buildWedges = (weights) => {
905
897
  ];
906
898
  };
907
899
  var Pie = ({ palette, weights = defaultWeights, size = SIZE2 }) => /* @__PURE__ */ jsx14(View5, { style: [styles5.pie, { borderRadius: size / 2, height: size, width: size }], children: buildWedges(weights).map(({ key, rotate, share }) => /* @__PURE__ */ jsx14(View5, { style: [styles5.rotator, { transform: [{ rotate: `${rotate}deg` }] }], children: /* @__PURE__ */ jsx14(View5, { style: styles5.halfMask, children: /* @__PURE__ */ jsx14(View5, { style: [styles5.innerRotator, { transform: [{ rotate: `${share - 180}deg` }] }], children: /* @__PURE__ */ jsx14(View5, { style: [styles5.wedgeFill, { backgroundColor: palette[key] }] }) }) }) }, key)) });
908
- var PalettePicker = ({ value, onChange, harmony = "split-complementary", weights, colors = defaultColors, blur: blurProp }) => {
900
+ var PalettePicker = ({ value, onChange, harmony = "split-complementary", onHarmonyChange, weights, colors = defaultColors, blur: blurProp }) => {
909
901
  const blur = useBlur(blurProp);
910
902
  const { colors: themeColors } = useTheme8();
911
- const [open, setOpen] = useState7(false);
903
+ const [open, setOpen] = useState6(false);
912
904
  const { onLayout, size } = useSwatchSize(SIZE2);
913
905
  return /* @__PURE__ */ jsxs6(Fragment3, { children: [
914
906
  /* @__PURE__ */ jsx14(TouchableRipple2, { onPress: () => setOpen(true), style: styles5.trigger, borderless: true, children: /* @__PURE__ */ jsxs6(View5, { style: styles5.triggerContent, children: [
915
907
  /* @__PURE__ */ jsx14(Pie, { palette: getTriadicPalette(value, harmony), weights }),
916
908
  /* @__PURE__ */ jsx14(View5, { pointerEvents: "none", style: [styles5.triggerIcon, { borderRadius: SIZE2 / 2 }, getSwatchRing(value, false, themeColors.outlineVariant)], children: /* @__PURE__ */ jsx14(Icon2, { source: "palette", size: 20, color: getContrastColor(value) }) })
917
909
  ] }) }),
918
- /* @__PURE__ */ jsx14(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ jsx14(Dialog.Content, { children: /* @__PURE__ */ jsx14(View5, { style: styles5.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ jsxs6(
919
- TouchableOpacity2,
920
- {
921
- onPress: () => {
922
- onChange(hex);
923
- setOpen(false);
910
+ /* @__PURE__ */ jsx14(Dialog, { blur, visible: open, onDismiss: () => setOpen(false), children: /* @__PURE__ */ jsxs6(Dialog.Content, { children: [
911
+ onHarmonyChange && /* @__PURE__ */ jsx14(View5, { style: styles5.harmonyRow, children: /* @__PURE__ */ jsx14(HarmonyPicker, { value: harmony, onChange: onHarmonyChange, showLabels: false }) }),
912
+ /* @__PURE__ */ jsx14(View5, { style: styles5.swatches, onLayout, children: colors.map(({ value: hex }) => /* @__PURE__ */ jsxs6(
913
+ TouchableOpacity2,
914
+ {
915
+ onPress: () => {
916
+ onChange(hex);
917
+ setOpen(false);
918
+ },
919
+ style: [styles5.swatch, { height: size, width: size }],
920
+ children: [
921
+ /* @__PURE__ */ jsx14(Pie, { palette: getTriadicPalette(hex, harmony), weights, size }),
922
+ /* @__PURE__ */ jsx14(View5, { pointerEvents: "none", style: [styles5.ring, { borderRadius: size / 2 }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)], children: value === hex && /* @__PURE__ */ jsx14(Icon2, { source: "check", size: 20, color: getContrastColor(hex) }) })
923
+ ]
924
924
  },
925
- style: [styles5.swatch, { height: size, width: size }],
926
- children: [
927
- /* @__PURE__ */ jsx14(Pie, { palette: getTriadicPalette(hex, harmony), weights, size }),
928
- /* @__PURE__ */ jsx14(View5, { pointerEvents: "none", style: [styles5.ring, { borderRadius: size / 2 }, getSwatchRing(hex, value === hex, themeColors.outlineVariant)], children: value === hex && /* @__PURE__ */ jsx14(Icon2, { source: "check", size: 20, color: getContrastColor(hex) }) })
929
- ]
930
- },
931
- hex
932
- )) }) }) })
925
+ hex
926
+ )) })
927
+ ] }) })
933
928
  ] });
934
929
  };
935
930
  var styles5 = StyleSheet6.create({
936
931
  halfMask: { height: "100%", left: "50%", overflow: "hidden", position: "absolute", top: 0, width: "50%" },
932
+ harmonyRow: { marginBottom: 4 },
937
933
  innerRotator: { height: "100%", left: "-100%", overflow: "hidden", position: "absolute", top: 0, width: "200%" },
938
934
  pie: { overflow: "hidden" },
939
935
  rotator: { height: "100%", left: 0, overflow: "hidden", position: "absolute", top: 0, width: "100%" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rific/auto-paper",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "Adaptive react-native-paper theming: triadic palette from a seed color + system/light/dark appearance",
5
5
  "keywords": [
6
6
  "react-native",
@@ -1,9 +1,6 @@
1
- import { useState } from 'react'
2
- import { Button } from 'react-native-paper'
1
+ import { SegmentedButtons } from 'react-native-paper'
3
2
 
4
- import { useBlur } from '../BlurContext'
5
3
  import type { ColorHarmony } from '../utils/getTriadicPalette'
6
- import { Menu } from './Menu'
7
4
 
8
5
  const HARMONIES: { label: string; value: ColorHarmony; icon: string }[] = [
9
6
  { label: 'Triadic', value: 'triadic', icon: 'triangle-outline' },
@@ -14,39 +11,52 @@ const HARMONIES: { label: string; value: ColorHarmony; icon: string }[] = [
14
11
  { label: 'Dbl Split', value: 'double-split', icon: 'arrow-expand-horizontal' }
15
12
  ]
16
13
 
14
+ // Paper's own SegmentedButtonItem hardcodes `minWidth: 76` per segment (sized for a labeled text
15
+ // button), which 6 of never fit inside PalettePicker's own dialog card (capped at 400, and
16
+ // narrower still once its padding and safe-area margins are subtracted) — they just overflow past
17
+ // its `overflow: hidden` edge instead of shrinking. A fixed smaller guess (say 44) only moves the
18
+ // breakpoint rather than removing it — 6x44 still exceeds the card's content width on an ordinary
19
+ // ~360-375px phone. Zeroing the floor instead lets each segment's own `flex: 1` divide whatever
20
+ // width IS available with nothing left to enforce a minimum, so 6 of them mathematically can never
21
+ // exceed their container at any screen size — same "shrink to fit, never overflow" contract
22
+ // useSwatchGrid.ts's own SWATCH_GRID_COLUMNS already uses for the swatches below this row. Left
23
+ // untouched in labeled mode, where the wider floor is still doing its job of fitting the text.
24
+ const COMPACT_MIN_WIDTH = 0
25
+
17
26
  type Props = {
18
27
  value: ColorHarmony
19
28
  onChange: (harmony: ColorHarmony) => void
20
- blur?: boolean
29
+ // Hides text labels, leaving just the icon per segment — mirrors AppearancePicker's own
30
+ // showLabels, useful here since there are 6 segments to fit instead of 3.
31
+ // accessibilityLabel is always set regardless, so screen readers still announce the full
32
+ // harmony name either way.
33
+ showLabels?: boolean
34
+ // Per-segment fill, e.g. theme.colors.secondary/onSecondary for the checked segment and
35
+ // secondaryContainer/onSecondaryContainer for the rest — matches Paper's own checked/
36
+ // unchecked + container color vocabulary. Omit to fall back to Paper's default segmented-
37
+ // button styling.
38
+ checkedColor?: string
39
+ uncheckedColor?: string
40
+ checkedContainerColor?: string
41
+ uncheckedContainerColor?: string
21
42
  }
22
43
 
23
- export const HarmonyPicker = ({ value, onChange, blur: blurProp }: Props) => {
24
- const blur = useBlur(blurProp)
25
- const [open, setOpen] = useState(false)
26
- const selected = HARMONIES.find((h) => h.value === value)!
27
-
28
- return (
29
- <Menu
30
- blur={blur}
31
- visible={open}
32
- onDismiss={() => setOpen(false)}
33
- anchor={
34
- <Button mode='outlined' icon={selected.icon} onPress={() => setOpen(true)}>
35
- {selected.label}
36
- </Button>
44
+ export const HarmonyPicker = ({ value, onChange, showLabels = true, checkedColor, uncheckedColor, checkedContainerColor, uncheckedContainerColor }: Props) => (
45
+ <SegmentedButtons
46
+ value={value}
47
+ onValueChange={(v) => onChange(v as ColorHarmony)}
48
+ buttons={HARMONIES.map((h) => {
49
+ const checked = h.value === value
50
+ const containerColor = checked ? checkedContainerColor : uncheckedContainerColor
51
+ return {
52
+ value: h.value,
53
+ icon: h.icon,
54
+ label: showLabels ? h.label : undefined,
55
+ accessibilityLabel: h.label,
56
+ checkedColor,
57
+ uncheckedColor,
58
+ style: [showLabels ? undefined : { minWidth: COMPACT_MIN_WIDTH }, containerColor ? { backgroundColor: containerColor } : undefined]
37
59
  }
38
- >
39
- {HARMONIES.map((h) => (
40
- <Menu.Item
41
- key={h.value}
42
- leadingIcon={h.value === value ? 'check' : h.icon}
43
- title={h.label}
44
- onPress={() => {
45
- onChange(h.value)
46
- setOpen(false)
47
- }}
48
- />
49
- ))}
50
- </Menu>
51
- )
52
- }
60
+ })}
61
+ />
62
+ )
@@ -10,6 +10,7 @@ import { getTriadicPalette } from '../utils/getTriadicPalette'
10
10
  import type { SeedColor } from './ColorPicker'
11
11
  import { defaultColors } from './ColorPicker'
12
12
  import { Dialog } from './Dialog'
13
+ import { HarmonyPicker } from './HarmonyPicker'
13
14
 
14
15
  const SIZE = 48
15
16
 
@@ -55,12 +56,18 @@ type Props = {
55
56
  value: string
56
57
  onChange: (color: string) => void
57
58
  harmony?: ColorHarmony
59
+ // Opt-in: passing this turns on a compact, icon-only HarmonyPicker row inside the dialog, above
60
+ // the swatch grid, so every swatch's own Pie preview reshapes live as harmony changes. Omit it
61
+ // and this renders exactly as a plain PalettePicker always has — same trigger, same dialog,
62
+ // harmony fixed at whatever the `harmony` prop says. Gated on the callback itself rather than a
63
+ // separate boolean so there's no way to pass one without the other.
64
+ onHarmonyChange?: (harmony: ColorHarmony) => void
58
65
  weights?: PaletteWeights
59
66
  colors?: SeedColor[]
60
67
  blur?: boolean
61
68
  }
62
69
 
63
- export const PalettePicker = ({ value, onChange, harmony = 'split-complementary', weights, colors = defaultColors, blur: blurProp }: Props) => {
70
+ export const PalettePicker = ({ value, onChange, harmony = 'split-complementary', onHarmonyChange, weights, colors = defaultColors, blur: blurProp }: Props) => {
64
71
  const blur = useBlur(blurProp)
65
72
  const { colors: themeColors } = useTheme()
66
73
  const [open, setOpen] = useState(false)
@@ -79,6 +86,11 @@ export const PalettePicker = ({ value, onChange, harmony = 'split-complementary'
79
86
 
80
87
  <Dialog blur={blur} visible={open} onDismiss={() => setOpen(false)}>
81
88
  <Dialog.Content>
89
+ {onHarmonyChange && (
90
+ <View style={styles.harmonyRow}>
91
+ <HarmonyPicker value={harmony} onChange={onHarmonyChange} showLabels={false} />
92
+ </View>
93
+ )}
82
94
  <View style={styles.swatches} onLayout={onLayout}>
83
95
  {colors.map(({ value: hex }) => (
84
96
  <TouchableOpacity
@@ -104,6 +116,7 @@ export const PalettePicker = ({ value, onChange, harmony = 'split-complementary'
104
116
 
105
117
  const styles = StyleSheet.create({
106
118
  halfMask: { height: '100%', left: '50%', overflow: 'hidden', position: 'absolute', top: 0, width: '50%' },
119
+ harmonyRow: { marginBottom: 4 },
107
120
  innerRotator: { height: '100%', left: '-100%', overflow: 'hidden', position: 'absolute', top: 0, width: '200%' },
108
121
  pie: { overflow: 'hidden' },
109
122
  rotator: { height: '100%', left: 0, overflow: 'hidden', position: 'absolute', top: 0, width: '100%' },