@povio/ui 2.1.0 → 2.1.1

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.
@@ -33,7 +33,7 @@ const SelectListBoxItem = ({
33
33
  isDisabled,
34
34
  className: clsx(
35
35
  selectListBoxItemClass,
36
- !isMultiple && "selected:bg-interactive-contained-primary-idle selected:text-interactive-text-secondary-idle-inverted",
36
+ !isMultiple && "selected:bg-interactive-contained-primary-idle selected:text-interactive-contained-primary-on-idle",
37
37
  isNewItem ? "text-interactive-text-primary-idle" : "text-interactive-text-secondary-idle"
38
38
  ),
39
39
  children: [
@@ -41,7 +41,7 @@ const SelectListBoxItem = ({
41
41
  CheckboxCheckmark,
42
42
  {
43
43
  variant: "default",
44
- className: "group-focus-visible:!outline-none"
44
+ className: "group-focus-visible:outline-none!"
45
45
  }
46
46
  ),
47
47
  isMultiple && isSearchable && /* @__PURE__ */ jsx(
@@ -54,7 +54,7 @@ const SelectListBoxItem = ({
54
54
  CheckboxCheckmark,
55
55
  {
56
56
  variant: "default",
57
- className: "group-focus-visible:!outline-none"
57
+ className: "group-focus-visible:outline-none!"
58
58
  }
59
59
  )
60
60
  }
@@ -0,0 +1,17 @@
1
+ import { PropsWithChildren } from 'react';
2
+ import { z } from 'zod';
3
+ export declare namespace ThemeContext {
4
+ const ThemeSchema: z.ZodLiteral<"dark" | "light" | "system">;
5
+ export type Theme = z.infer<typeof ThemeSchema>;
6
+ interface ThemeContextValue {
7
+ theme: Theme;
8
+ systemTheme?: Exclude<Theme, "system">;
9
+ updateTheme: (theme: Theme) => void;
10
+ }
11
+ interface ThemeContextProviderProps {
12
+ storageKey?: string;
13
+ }
14
+ export const ThemeContextProvider: ({ children, storageKey, }: PropsWithChildren<ThemeContextProviderProps>) => import("react/jsx-runtime").JSX.Element;
15
+ export const useTheme: () => ThemeContextValue;
16
+ export {};
17
+ }
@@ -0,0 +1,69 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, useState, useCallback, useEffect, useMemo, use } from "react";
3
+ import { z } from "zod";
4
+ import { useLocalStorage } from "../hooks/useLocalStorage.js";
5
+ var ThemeContext;
6
+ ((_ThemeContext) => {
7
+ const DEFAULT_STORAGE_KEY = "theme";
8
+ const ThemeSchema = z.literal(["light", "dark", "system"]);
9
+ const ThemeContext2 = createContext(null);
10
+ _ThemeContext.ThemeContextProvider = ({
11
+ children,
12
+ storageKey = DEFAULT_STORAGE_KEY
13
+ }) => {
14
+ const [systemTheme, setSystemTheme] = useState(() => {
15
+ if (typeof window === "undefined") {
16
+ return void 0;
17
+ }
18
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
19
+ return media.matches ? "dark" : "light";
20
+ });
21
+ const { value: theme, set } = useLocalStorage({
22
+ key: storageKey,
23
+ schema: ThemeSchema
24
+ });
25
+ const updateTheme = useCallback(
26
+ (theme2) => {
27
+ set(theme2);
28
+ },
29
+ [set]
30
+ );
31
+ useEffect(() => {
32
+ if (typeof window === "undefined") {
33
+ return;
34
+ }
35
+ const onChange = (event) => {
36
+ setSystemTheme(event.matches ? "dark" : "light");
37
+ };
38
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
39
+ media.addEventListener("change", onChange);
40
+ return () => {
41
+ media.removeEventListener("change", onChange);
42
+ };
43
+ }, []);
44
+ useEffect(() => {
45
+ document.documentElement.classList.remove("dark");
46
+ if ((!theme || theme === "system") && systemTheme === "dark" || theme === "dark") {
47
+ document.documentElement.classList.add("dark");
48
+ } else if (theme === "light") {
49
+ document.documentElement.classList.add("light");
50
+ }
51
+ }, [theme, systemTheme]);
52
+ const contextValue = useMemo(
53
+ () => ({
54
+ theme: theme ?? "system",
55
+ systemTheme,
56
+ updateTheme
57
+ }),
58
+ [theme, systemTheme, updateTheme]
59
+ );
60
+ return /* @__PURE__ */ jsx(ThemeContext2.Provider, { value: contextValue, children });
61
+ };
62
+ _ThemeContext.useTheme = () => {
63
+ const context = use(ThemeContext2);
64
+ return context;
65
+ };
66
+ })(ThemeContext || (ThemeContext = {}));
67
+ export {
68
+ ThemeContext
69
+ };
@@ -1,17 +1,22 @@
1
1
  import { useMemo } from "react";
2
2
  import { useMediaQuery } from "react-responsive";
3
+ let breakpoints = null;
3
4
  function getBreakpoints() {
5
+ if (breakpoints) {
6
+ return breakpoints;
7
+ }
4
8
  const cs = getComputedStyle(document.documentElement);
5
9
  const entries = ["sm", "md", "lg", "xl", "2xl"].map((k) => [k, cs.getPropertyValue(`--breakpoint-${k}`).trim()]).filter(([, v]) => !!v);
6
- return Object.fromEntries(entries);
10
+ breakpoints = Object.fromEntries(entries);
11
+ return breakpoints;
7
12
  }
8
13
  function useBreakpoint(breakpointKey) {
9
- const breakpoints = useMemo(() => getBreakpoints(), []);
10
- if (!breakpoints) {
14
+ const breakpoints2 = useMemo(() => getBreakpoints(), []);
15
+ if (!breakpoints2) {
11
16
  throw new Error("Tailwind config is missing theme.screens");
12
17
  }
13
18
  const bool = useMediaQuery({
14
- query: `(min-width: ${breakpoints[breakpointKey]})`
19
+ query: `(min-width: ${breakpoints2[breakpointKey]})`
15
20
  });
16
21
  return bool;
17
22
  }
@@ -1,11 +1,7 @@
1
- import { z } from 'zod';
2
- type FilterValue = string | string[] | boolean | number;
3
1
  export interface FilterStore<TFilterData> {
4
2
  filterData: TFilterData;
5
3
  setFilterValue: (data: Partial<TFilterData>) => void;
6
4
  getFilterValue: (keys: (keyof TFilterData)[]) => Partial<TFilterData>;
7
5
  clearAllFilters: () => void;
8
6
  }
9
- export declare function parseFilterFromQuery(query: Record<string, any>, schema?: z.ZodObject<any>): Record<string, FilterValue>;
10
- export declare function useFilters<TFilterData>(defaultFilterValues?: TFilterData, prefix?: string, schema?: z.ZodObject<any>): FilterStore<TFilterData>;
11
- export {};
7
+ export declare const useFilters: <TFilterData>(defaultFilterValues?: TFilterData, prefix?: string) => FilterStore<TFilterData>;
@@ -1,160 +1,107 @@
1
- import { useNavigate, useLocation } from "@tanstack/react-router";
2
- import { useMemo, useRef, useState, useEffect } from "react";
3
- import { z } from "zod";
4
- function getFieldType(schema, fieldKey) {
5
- if (!schema || !schema.shape) return "unknown";
6
- let fieldSchema = schema.shape[fieldKey];
7
- while (fieldSchema) {
8
- if (fieldSchema instanceof z.ZodOptional || fieldSchema instanceof z.ZodNullable) {
9
- fieldSchema = fieldSchema.unwrap();
10
- } else {
11
- break;
12
- }
13
- }
14
- if (!fieldSchema) return "unknown";
15
- if (fieldSchema instanceof z.ZodNumber) return "number";
16
- if (fieldSchema instanceof z.ZodBoolean) return "boolean";
17
- if (fieldSchema instanceof z.ZodString) return "string";
18
- return "unknown";
19
- }
20
- function serializeFiltersToQuery(filterData, prefix) {
1
+ import { useState, useEffect } from "react";
2
+ import { UIRouter } from "../config/router.context.js";
3
+ const serializeFiltersToQuery = (filterData, prefix) => {
21
4
  const query = {};
22
5
  for (const [key, value] of Object.entries(filterData)) {
23
6
  if (value === null || value === void 0) {
24
7
  continue;
25
8
  }
9
+ const filterKey = `filter[${prefix && `${prefix}-`}${key}]`;
26
10
  if (Array.isArray(value) || typeof value === "object") {
27
- query[`filter[${prefix && `${prefix}-`}${key}]`] = JSON.stringify(value);
11
+ query[filterKey] = JSON.stringify(value);
28
12
  } else if (typeof value === "boolean") {
29
- query[`filter[${prefix && `${prefix}-`}${key}]`] = value ? "true" : "false";
30
- } else if (typeof value === "number") {
31
- query[`filter[${prefix && `${prefix}-`}${key}]`] = value.toString();
13
+ query[filterKey] = value ? "true" : "false";
32
14
  } else {
33
- query[`filter[${prefix && `${prefix}-`}${key}]`] = value;
15
+ query[filterKey] = value;
34
16
  }
35
17
  }
36
18
  return query;
37
- }
38
- function parseFilterFromQuery(query, schema) {
19
+ };
20
+ const parseFilterFromQuery = (query) => {
39
21
  const filter = {};
40
22
  for (const [key, value] of Object.entries(query)) {
41
- const match = key.match(/^filter\[(.+?)\]$/);
42
- if (match) {
43
- const filterKey = match[1];
44
- const baseKey = filterKey.includes("-") ? filterKey.split("-").pop() || filterKey : filterKey;
45
- if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}")) {
46
- filter[filterKey] = JSON.parse(value);
47
- } else if (["true", "false"].includes(value)) {
48
- filter[filterKey] = value === "true";
49
- } else {
50
- const expectedType = getFieldType(schema, baseKey);
51
- if (expectedType === "number" && !Number.isNaN(Number(value)) && value !== "" && value !== null) {
52
- filter[filterKey] = Number(value);
53
- } else if (expectedType === "boolean" && ["true", "false"].includes(value)) {
54
- filter[filterKey] = value === "true";
55
- } else {
56
- filter[filterKey] = value;
57
- }
58
- }
23
+ const match = /^filter\[(.+?)\]$/.exec(key);
24
+ if (!match) {
25
+ continue;
26
+ }
27
+ const filterKey = match[1];
28
+ const isArray = value.startsWith("[") && value.endsWith("]");
29
+ const isObject = value.startsWith("{") && value.endsWith("}");
30
+ const isNumber = !Number.isNaN(Number(value)) && value !== "";
31
+ const isBoolean = ["true", "false"].includes(value);
32
+ if (isArray || isObject || isNumber) {
33
+ filter[filterKey] = JSON.parse(value);
34
+ } else if (isBoolean) {
35
+ filter[filterKey] = value === "true";
36
+ } else {
37
+ filter[filterKey] = value;
59
38
  }
60
39
  }
61
40
  return filter;
62
- }
63
- function useFilters(defaultFilterValues, prefix = "", schema) {
64
- const navigate = useNavigate();
65
- const location = useLocation();
66
- const queryObject = useMemo(
67
- () => Object.fromEntries(new URLSearchParams(location.searchStr || "").entries()),
68
- [location.searchStr]
69
- );
70
- const hasAppliedDefaultsRef = useRef(false);
71
- const lastPathnameRef = useRef(location.pathname);
72
- const queryFilters = parseFilterFromQuery(queryObject, schema);
41
+ };
42
+ const useFilters = (defaultFilterValues, prefix = "") => {
43
+ const { query, pathname, replace } = UIRouter.useUIRouter();
44
+ const queryFilters = parseFilterFromQuery(query);
73
45
  const [filterData, setFilterData] = useState(queryFilters);
74
46
  useEffect(() => {
75
- if (lastPathnameRef.current !== location.pathname) {
76
- hasAppliedDefaultsRef.current = false;
77
- lastPathnameRef.current = location.pathname;
78
- }
79
- }, [location.pathname]);
80
- useEffect(() => {
81
- const currentQueryObject = Object.fromEntries(new URLSearchParams(location.searchStr || "").entries());
82
- const currentFilters = parseFilterFromQuery(currentQueryObject, schema);
83
- const shouldApplyDefaults = defaultFilterValues && Object.keys(currentFilters).length === 0 && !hasAppliedDefaultsRef.current;
84
- if (shouldApplyDefaults) {
47
+ const setUrlToDefaultFilters = () => {
85
48
  const flatFilterQuery = serializeFiltersToQuery(defaultFilterValues, prefix);
86
- const sp = new URLSearchParams(location.searchStr || "");
87
- let needsNavigation = false;
88
- Object.entries(flatFilterQuery).forEach(([k, v]) => {
89
- const currentValue = sp.get(k);
90
- if (Array.isArray(v)) {
91
- const currentValues = sp.getAll(k);
92
- if (JSON.stringify(currentValues.sort()) !== JSON.stringify(v.sort())) {
93
- needsNavigation = true;
94
- }
95
- } else {
96
- if (currentValue !== v) {
97
- needsNavigation = true;
98
- }
49
+ replace({
50
+ pathname,
51
+ query: {
52
+ ...query,
53
+ ...flatFilterQuery
99
54
  }
100
55
  });
101
- if (needsNavigation) {
102
- Object.entries(flatFilterQuery).forEach(([k, v]) => {
103
- sp.delete(k);
104
- if (Array.isArray(v)) {
105
- v.forEach((val) => sp.append(k, val));
106
- } else {
107
- sp.set(k, v);
108
- }
109
- });
110
- const to = `${location.pathname}${sp.toString() ? `?${sp.toString()}` : ""}`;
111
- navigate({ to, replace: true });
112
- }
113
- hasAppliedDefaultsRef.current = true;
56
+ };
57
+ const currentFilters = parseFilterFromQuery(query);
58
+ const hasFiltersSet = Object.keys(currentFilters).length > 0;
59
+ if (defaultFilterValues && !hasFiltersSet) {
60
+ setUrlToDefaultFilters();
114
61
  }
115
- }, [defaultFilterValues, prefix, location.pathname, location.searchStr]);
62
+ }, [defaultFilterValues, prefix]);
116
63
  useEffect(() => {
117
- const newQueryFilters = parseFilterFromQuery(queryObject, schema);
64
+ const newQueryFilters = parseFilterFromQuery(query);
118
65
  setFilterData(newQueryFilters);
119
- }, [queryObject, schema]);
66
+ }, [query]);
120
67
  const setFilterValue = (data) => {
121
- const next = { ...filterData };
68
+ let newFilters = { ...filterData };
122
69
  const isReset = Object.keys(data).length === 0;
123
70
  const isResetToDefault = data === defaultFilterValues;
124
71
  if (isResetToDefault) {
125
- Object.keys(next).forEach((key) => delete next[key]);
126
- Object.entries(defaultFilterValues ?? {}).forEach(([key, value]) => {
127
- next[key] = value;
128
- });
129
- } else if (!isReset) {
72
+ newFilters = {
73
+ ...defaultFilterValues
74
+ };
75
+ } else if (isReset) {
76
+ newFilters = {};
77
+ } else {
130
78
  Object.entries(data).forEach(([key, value]) => {
131
79
  if (value === void 0) {
132
- delete next[key];
80
+ delete newFilters[key];
133
81
  } else {
134
- next[key] = value;
82
+ newFilters[key] = value;
135
83
  }
136
84
  });
137
- } else {
138
- Object.keys(next).forEach((key) => delete next[key]);
139
85
  }
140
- const sp = new URLSearchParams(location.searchStr || "");
141
- Array.from(sp.keys()).forEach((key) => {
142
- if (key.startsWith(`filter[${prefix && `${prefix}-`}`)) {
143
- sp.delete(key);
144
- }
145
- });
146
- const flatFilterQuery = serializeFiltersToQuery(next, prefix);
147
- Object.entries(flatFilterQuery).forEach(([k, v]) => {
148
- sp.delete(k);
149
- if (Array.isArray(v)) {
150
- v.forEach((val) => sp.append(k, val));
151
- } else {
152
- sp.set(k, v);
86
+ const prefixPart = prefix ? `${prefix}-` : "";
87
+ const cleanedQuery = Object.keys(query).reduce(
88
+ (acc, k) => {
89
+ if (!k.startsWith(`filter[${prefixPart}`)) {
90
+ acc[k] = query[k];
91
+ }
92
+ return acc;
93
+ },
94
+ {}
95
+ );
96
+ const flatFilterQuery = serializeFiltersToQuery(newFilters, prefix);
97
+ replace({
98
+ pathname,
99
+ query: {
100
+ ...cleanedQuery,
101
+ ...flatFilterQuery
153
102
  }
154
103
  });
155
- const to = `${location.pathname}${sp.toString() ? `?${sp.toString()}` : ""}`;
156
- navigate({ to, replace: true });
157
- setFilterData(next);
104
+ setFilterData(newFilters);
158
105
  };
159
106
  const getFilterValue = (keys) => {
160
107
  const result = {};
@@ -167,8 +114,7 @@ function useFilters(defaultFilterValues, prefix = "", schema) {
167
114
  setFilterValue(defaultFilterValues ?? {});
168
115
  };
169
116
  return { filterData, setFilterValue, getFilterValue, clearAllFilters };
170
- }
117
+ };
171
118
  export {
172
- parseFilterFromQuery,
173
119
  useFilters
174
120
  };
@@ -7,7 +7,5 @@ export declare const useLocalStorage: <ZOutput, ZInput>({ key, schema }: IProps<
7
7
  value: ZOutput | null;
8
8
  set: (newValue: ZInput) => void;
9
9
  remove: () => void;
10
- error: Error | null;
11
- isInitialLoading: boolean;
12
10
  };
13
11
  export {};
@@ -1,36 +1,27 @@
1
- import { useState, useEffect, useCallback } from "react";
1
+ import { useState, useCallback } from "react";
2
+ const getValue = (key, schema) => {
3
+ if (!localStorage) {
4
+ return null;
5
+ }
6
+ const lsValue = localStorage.getItem(key);
7
+ if (!lsValue) {
8
+ return null;
9
+ }
10
+ let jsonOrString;
11
+ try {
12
+ jsonOrString = JSON.parse(lsValue);
13
+ } catch {
14
+ jsonOrString = lsValue;
15
+ }
16
+ const parsedValue = schema.safeParse(jsonOrString);
17
+ if (parsedValue.success) {
18
+ return parsedValue.data;
19
+ } else {
20
+ return null;
21
+ }
22
+ };
2
23
  const useLocalStorage = ({ key, schema }) => {
3
- const [value, setValue] = useState(null);
4
- const [isInitialLoading, setIsInitialLoading] = useState(true);
5
- const [error, setError] = useState(null);
6
- useEffect(() => {
7
- if (!localStorage) {
8
- setValue(null);
9
- return;
10
- }
11
- try {
12
- const lsValue = localStorage.getItem(key);
13
- if (!lsValue) {
14
- setValue(null);
15
- return;
16
- }
17
- let jsonOrString;
18
- try {
19
- jsonOrString = JSON.parse(lsValue);
20
- } catch {
21
- jsonOrString = lsValue;
22
- }
23
- const parsedValue = schema.safeParse(jsonOrString);
24
- if (parsedValue.success) {
25
- setValue(parsedValue.data);
26
- } else {
27
- setError(parsedValue.error);
28
- setValue(null);
29
- }
30
- } finally {
31
- setIsInitialLoading(false);
32
- }
33
- }, [key, schema]);
24
+ const [value, setValue] = useState(() => getValue(key, schema));
34
25
  const set = useCallback(
35
26
  (newValue) => {
36
27
  if (!localStorage) {
@@ -56,7 +47,7 @@ const useLocalStorage = ({ key, schema }) => {
56
47
  localStorage.removeItem(key);
57
48
  setValue(null);
58
49
  }, [key]);
59
- return { value, set, remove, error, isInitialLoading };
50
+ return { value, set, remove };
60
51
  };
61
52
  export {
62
53
  useLocalStorage
package/dist/index.d.ts CHANGED
@@ -157,6 +157,7 @@ export { Confirmation } from './config/confirmation.context';
157
157
  export { ns, resources } from './config/i18n';
158
158
  export { LinkContext } from './config/link.context';
159
159
  export { UIRouter } from './config/router.context';
160
+ export { ThemeContext } from './config/theme.context';
160
161
  export { UIConfig } from './config/uiConfig.context';
161
162
  export { UIStyle } from './config/uiStyle.context';
162
163
  export type { DynamicColumnsOptions } from './helpers/dynamicColumns';
package/dist/index.js CHANGED
@@ -96,6 +96,7 @@ import { Confirmation } from "./config/confirmation.context.js";
96
96
  import { ns, resources } from "./config/i18n.js";
97
97
  import { LinkContext } from "./config/link.context.js";
98
98
  import { UIRouter } from "./config/router.context.js";
99
+ import { ThemeContext } from "./config/theme.context.js";
99
100
  import { UIConfig } from "./config/uiConfig.context.js";
100
101
  import { UIStyle } from "./config/uiStyle.context.js";
101
102
  import { dynamicColumns } from "./helpers/dynamicColumns.js";
@@ -240,6 +241,7 @@ export {
240
241
  TextColorIcon,
241
242
  TextEditor,
242
243
  TextInput,
244
+ ThemeContext,
243
245
  TimePicker,
244
246
  Toast,
245
247
  ToastContainer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@povio/ui",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
5
  "module": "dist/index.js",
6
6
  "types": "dist/index.d.ts",