tekivex-ui 2.2.0 → 2.5.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.
Files changed (129) hide show
  1. package/dist/TkxForm-BWK4LqY3.js +1461 -0
  2. package/dist/TkxForm-ljQjX7KD.cjs +15 -0
  3. package/dist/charts.cjs +1 -0
  4. package/dist/charts.js +480 -0
  5. package/dist/headless.cjs +1 -0
  6. package/dist/headless.js +258 -0
  7. package/dist/index-BINBzXuY.cjs +2 -0
  8. package/dist/index-eT_U4qB2.js +512 -0
  9. package/dist/index.cjs +63 -14
  10. package/dist/index.d.ts +33 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +11396 -6336
  13. package/dist/src/charts/TkxAreaChart.d.ts +29 -0
  14. package/dist/src/charts/TkxAreaChart.d.ts.map +1 -0
  15. package/dist/src/charts/TkxBarChart.d.ts +28 -0
  16. package/dist/src/charts/TkxBarChart.d.ts.map +1 -0
  17. package/dist/src/charts/TkxDonutChart.d.ts +23 -0
  18. package/dist/src/charts/TkxDonutChart.d.ts.map +1 -0
  19. package/dist/src/charts/TkxLineChart.d.ts +35 -0
  20. package/dist/src/charts/TkxLineChart.d.ts.map +1 -0
  21. package/dist/src/charts/TkxPieChart.d.ts +19 -0
  22. package/dist/src/charts/TkxPieChart.d.ts.map +1 -0
  23. package/dist/src/charts/TkxRadarChart.d.ts +20 -0
  24. package/dist/src/charts/TkxRadarChart.d.ts.map +1 -0
  25. package/dist/src/charts/TkxScatterChart.d.ts +28 -0
  26. package/dist/src/charts/TkxScatterChart.d.ts.map +1 -0
  27. package/dist/src/charts/index.d.ts +15 -0
  28. package/dist/src/charts/index.d.ts.map +1 -0
  29. package/dist/src/charts/shared.d.ts +33 -0
  30. package/dist/src/charts/shared.d.ts.map +1 -0
  31. package/dist/src/components/TkxAffix.d.ts +10 -0
  32. package/dist/src/components/TkxAffix.d.ts.map +1 -0
  33. package/dist/src/components/TkxAnchor.d.ts +14 -0
  34. package/dist/src/components/TkxAnchor.d.ts.map +1 -0
  35. package/dist/src/components/TkxCascader.d.ts +16 -0
  36. package/dist/src/components/TkxCascader.d.ts.map +1 -0
  37. package/dist/src/components/TkxDataGrid.d.ts +19 -1
  38. package/dist/src/components/TkxDataGrid.d.ts.map +1 -1
  39. package/dist/src/components/TkxForm.d.ts +33 -3
  40. package/dist/src/components/TkxForm.d.ts.map +1 -1
  41. package/dist/src/components/TkxImage.d.ts.map +1 -1
  42. package/dist/src/components/TkxList.d.ts +24 -0
  43. package/dist/src/components/TkxList.d.ts.map +1 -0
  44. package/dist/src/components/TkxMentions.d.ts +15 -0
  45. package/dist/src/components/TkxMentions.d.ts.map +1 -0
  46. package/dist/src/components/TkxQRCode.d.ts +11 -0
  47. package/dist/src/components/TkxQRCode.d.ts.map +1 -0
  48. package/dist/src/components/TkxResult.d.ts +11 -0
  49. package/dist/src/components/TkxResult.d.ts.map +1 -0
  50. package/dist/src/components/TkxSegmented.d.ts +16 -0
  51. package/dist/src/components/TkxSegmented.d.ts.map +1 -0
  52. package/dist/src/components/TkxSelect.d.ts +5 -1
  53. package/dist/src/components/TkxSelect.d.ts.map +1 -1
  54. package/dist/src/components/TkxTour.d.ts +15 -0
  55. package/dist/src/components/TkxTour.d.ts.map +1 -0
  56. package/dist/src/components/TkxTypography.d.ts +1 -1
  57. package/dist/src/components/TkxTypography.d.ts.map +1 -1
  58. package/dist/src/components/TkxWatermark.d.ts +12 -0
  59. package/dist/src/components/TkxWatermark.d.ts.map +1 -0
  60. package/dist/src/components/index.d.ts +10 -0
  61. package/dist/src/components/index.d.ts.map +1 -1
  62. package/dist/src/engine/tkx.d.ts +38 -0
  63. package/dist/src/engine/tkx.d.ts.map +1 -1
  64. package/dist/src/headless/index.d.ts +18 -0
  65. package/dist/src/headless/index.d.ts.map +1 -0
  66. package/dist/src/headless/useControllable.d.ts +20 -0
  67. package/dist/src/headless/useControllable.d.ts.map +1 -0
  68. package/dist/src/headless/useDebounce.d.ts +14 -0
  69. package/dist/src/headless/useDebounce.d.ts.map +1 -0
  70. package/dist/src/headless/useDisclosure.d.ts +20 -0
  71. package/dist/src/headless/useDisclosure.d.ts.map +1 -0
  72. package/dist/src/headless/useFormState.d.ts +44 -0
  73. package/dist/src/headless/useFormState.d.ts.map +1 -0
  74. package/dist/src/headless/useIntersectionObserver.d.ts +23 -0
  75. package/dist/src/headless/useIntersectionObserver.d.ts.map +1 -0
  76. package/dist/src/headless/useListSelection.d.ts +29 -0
  77. package/dist/src/headless/useListSelection.d.ts.map +1 -0
  78. package/dist/src/headless/useLocalStorage.d.ts +9 -0
  79. package/dist/src/headless/useLocalStorage.d.ts.map +1 -0
  80. package/dist/src/headless/useMediaQuery.d.ts +21 -0
  81. package/dist/src/headless/useMediaQuery.d.ts.map +1 -0
  82. package/dist/src/headless/useRovingTabIndex.d.ts +33 -0
  83. package/dist/src/headless/useRovingTabIndex.d.ts.map +1 -0
  84. package/dist/src/headless/useThrottle.d.ts +9 -0
  85. package/dist/src/headless/useThrottle.d.ts.map +1 -0
  86. package/dist/src/i18n/index.d.ts +47 -2
  87. package/dist/src/i18n/index.d.ts.map +1 -1
  88. package/dist/src/themes/index.d.ts.map +1 -1
  89. package/package.json +61 -7
  90. package/src/charts/TkxAreaChart.tsx +119 -0
  91. package/src/charts/TkxBarChart.tsx +115 -0
  92. package/src/charts/TkxDonutChart.tsx +145 -0
  93. package/src/charts/TkxLineChart.tsx +135 -0
  94. package/src/charts/TkxPieChart.tsx +82 -0
  95. package/src/charts/TkxRadarChart.tsx +90 -0
  96. package/src/charts/TkxScatterChart.tsx +102 -0
  97. package/src/charts/index.ts +20 -0
  98. package/src/charts/shared.ts +45 -0
  99. package/src/components/TkxAffix.tsx +138 -0
  100. package/src/components/TkxAnchor.tsx +193 -0
  101. package/src/components/TkxCascader.tsx +276 -0
  102. package/src/components/TkxDataGrid.tsx +557 -230
  103. package/src/components/TkxForm.tsx +67 -15
  104. package/src/components/TkxImage.tsx +1 -1
  105. package/src/components/TkxList.tsx +242 -0
  106. package/src/components/TkxMentions.tsx +210 -0
  107. package/src/components/TkxQRCode.tsx +174 -0
  108. package/src/components/TkxResult.tsx +128 -0
  109. package/src/components/TkxSegmented.tsx +169 -0
  110. package/src/components/TkxSelect.tsx +209 -6
  111. package/src/components/TkxTour.tsx +239 -0
  112. package/src/components/TkxTypography.tsx +4 -2
  113. package/src/components/TkxWatermark.tsx +143 -0
  114. package/src/components/index.ts +10 -0
  115. package/src/engine/tkx.ts +122 -3
  116. package/src/headless/index.ts +44 -0
  117. package/src/headless/useControllable.ts +40 -0
  118. package/src/headless/useDebounce.ts +35 -0
  119. package/src/headless/useDisclosure.ts +28 -0
  120. package/src/headless/useFormState.ts +111 -0
  121. package/src/headless/useIntersectionObserver.ts +51 -0
  122. package/src/headless/useListSelection.ts +101 -0
  123. package/src/headless/useLocalStorage.ts +46 -0
  124. package/src/headless/useMediaQuery.ts +37 -0
  125. package/src/headless/useRovingTabIndex.ts +84 -0
  126. package/src/headless/useThrottle.ts +46 -0
  127. package/src/i18n/I18nProvider.tsx +1 -1
  128. package/src/i18n/index.ts +678 -38
  129. package/src/themes/index.ts +31 -4
package/src/engine/tkx.ts CHANGED
@@ -660,6 +660,10 @@ function resolveUtility(u: string): Declarations | null {
660
660
  // ── CSS variable passthrough: [--my-var:value] ────────────────────────────
661
661
  if ((m = u.match(/^\[(--[a-zA-Z0-9-]+):(.+)]$/))) { return { [m[1]]: m[2] }; }
662
662
 
663
+ // ── Plugin utilities ─────────────────────────────────────────────────────
664
+ const pluginResult = resolvePluginUtility(u);
665
+ if (pluginResult) return pluginResult;
666
+
663
667
  return null;
664
668
  }
665
669
 
@@ -705,6 +709,8 @@ function declarationsToCSS(decls: Declarations): string {
705
709
 
706
710
  const registry = new Map<string, string>(); // hash → full CSS block
707
711
  let styleEl: HTMLStyleElement | null = null;
712
+ // Constructable CSSStyleSheet for O(1) rule insertion (Chrome 73+, Firefox 101+, Safari 16.4+)
713
+ let adoptedSheet: CSSStyleSheet | null = null;
708
714
 
709
715
  function getOrCreateStyleEl(): HTMLStyleElement {
710
716
  if (!styleEl || !styleEl.isConnected) {
@@ -718,15 +724,66 @@ function getOrCreateStyleEl(): HTMLStyleElement {
718
724
  return styleEl;
719
725
  }
720
726
 
727
+ function getAdoptedSheet(): CSSStyleSheet | null {
728
+ if (adoptedSheet) return adoptedSheet;
729
+ try {
730
+ // Constructable Stylesheets API — supported in all evergreen browsers
731
+ const sheet = new CSSStyleSheet();
732
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
733
+ adoptedSheet = sheet;
734
+ return sheet;
735
+ } catch {
736
+ return null;
737
+ }
738
+ }
739
+
721
740
  function registerBlock(hash: string, cssBlock: string): void {
722
741
  if (registry.has(hash)) return;
723
742
  registry.set(hash, cssBlock);
724
- if (typeof document !== 'undefined') {
725
- const el = getOrCreateStyleEl();
726
- el.textContent += cssBlock + '\n';
743
+ if (typeof document === 'undefined') return;
744
+
745
+ const sheet = getAdoptedSheet();
746
+ if (sheet) {
747
+ // O(1) per rule — insertRule appends without touching existing text
748
+ // Split multi-rule blocks (e.g. base + media query) and insert each one.
749
+ // We use a simple regex split on top-level `}` boundaries.
750
+ const rules = splitCSSRules(cssBlock);
751
+ for (const rule of rules) {
752
+ try {
753
+ sheet.insertRule(rule, sheet.cssRules.length);
754
+ } catch {
755
+ // Fallback for any rule the browser rejects (e.g. invalid pseudo)
756
+ getOrCreateStyleEl().textContent += rule + '\n';
757
+ }
758
+ }
759
+ } else {
760
+ // Fallback for environments that don't support adoptedStyleSheets
761
+ getOrCreateStyleEl().textContent += cssBlock + '\n';
727
762
  }
728
763
  }
729
764
 
765
+ /**
766
+ * Split a CSS string into individual top-level rules.
767
+ * Handles nested braces (media queries, @supports, etc.).
768
+ */
769
+ function splitCSSRules(css: string): string[] {
770
+ const rules: string[] = [];
771
+ let depth = 0;
772
+ let start = 0;
773
+ for (let i = 0; i < css.length; i++) {
774
+ if (css[i] === '{') depth++;
775
+ else if (css[i] === '}') {
776
+ depth--;
777
+ if (depth === 0) {
778
+ const rule = css.slice(start, i + 1).trim();
779
+ if (rule) rules.push(rule);
780
+ start = i + 1;
781
+ }
782
+ }
783
+ }
784
+ return rules;
785
+ }
786
+
730
787
  // ── Main TKX function ─────────────────────────────────────────────────────────
731
788
 
732
789
  export type TkxInput = string | false | null | undefined | TkxInput[] | Record<string, boolean>;
@@ -862,6 +919,10 @@ export function extractAtomicCSS(): string {
862
919
  export function resetAtomicCSS(): void {
863
920
  registry.clear();
864
921
  styleEl = null;
922
+ if (adoptedSheet && typeof document !== 'undefined') {
923
+ document.adoptedStyleSheets = document.adoptedStyleSheets.filter(s => s !== adoptedSheet);
924
+ adoptedSheet = null;
925
+ }
865
926
  }
866
927
 
867
928
  // ── cx — className merge helper (like clsx but integrated) ────────────────────
@@ -872,3 +933,61 @@ export function resetAtomicCSS(): void {
872
933
  export function cx(...classes: (string | false | null | undefined)[]): string {
873
934
  return classes.filter(Boolean).join(' ');
874
935
  }
936
+
937
+ // ── Plugin System ────────────────────────────────────────────────────────────
938
+ /**
939
+ * TKX Plugin API — extend the utility engine with custom utilities.
940
+ *
941
+ * @example
942
+ * ```tsx
943
+ * import { tkxPlugin, tkx } from '@tekivex/ui';
944
+ *
945
+ * // Register custom utilities
946
+ * tkxPlugin({
947
+ * name: 'brand',
948
+ * utilities: {
949
+ * 'brand-gradient': { background: 'linear-gradient(135deg, #667eea, #764ba2)' },
950
+ * 'brand-text': { color: '#667eea', fontWeight: '700' },
951
+ * 'card-shadow': { boxShadow: '0 8px 32px rgba(102, 126, 234, 0.15)' },
952
+ * 'glass': {
953
+ * background: 'rgba(255, 255, 255, 0.05)',
954
+ * backdropFilter: 'blur(12px)',
955
+ * border: '1px solid rgba(255, 255, 255, 0.1)',
956
+ * },
957
+ * },
958
+ * });
959
+ *
960
+ * // Use them like any built-in utility
961
+ * const cls = tkx('brand-gradient p-6 rounded-xl card-shadow');
962
+ * ```
963
+ */
964
+
965
+ export interface TkxPluginDef {
966
+ name: string;
967
+ utilities: Record<string, Record<string, string>>;
968
+ }
969
+
970
+ const pluginRegistry = new Map<string, Record<string, Record<string, string>>>();
971
+
972
+ export function tkxPlugin(plugin: TkxPluginDef): void {
973
+ pluginRegistry.set(plugin.name, plugin.utilities);
974
+ }
975
+
976
+ export function tkxRemovePlugin(name: string): void {
977
+ pluginRegistry.delete(name);
978
+ }
979
+
980
+ export function tkxListPlugins(): string[] {
981
+ return Array.from(pluginRegistry.keys());
982
+ }
983
+
984
+ /**
985
+ * Resolve a plugin utility to CSS properties.
986
+ * Called internally by the TKX engine — but also exported for inspection.
987
+ */
988
+ export function resolvePluginUtility(name: string): Record<string, string> | null {
989
+ for (const utilities of pluginRegistry.values()) {
990
+ if (utilities[name]) return utilities[name];
991
+ }
992
+ return null;
993
+ }
@@ -0,0 +1,44 @@
1
+ // ── @tekivex/ui/headless ─────────────────────────────────────────────────────
2
+ // Behavior-only primitives, hooks, and utilities — zero styles shipped.
3
+ // Use these to build completely custom UI on top of TekiVex's battle-tested
4
+ // accessibility, form, and interaction logic.
5
+ //
6
+ // Import from '@tekivex/ui/headless' — no CSS, no theme tokens, no components.
7
+ //
8
+ // @example
9
+ // import { useDisclosure, useFocusTrap, useFormState, useRovingTabIndex } from '@tekivex/ui/headless';
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+
12
+ // ── Accessibility hooks ──────────────────────────────────────────────────────
13
+ export {
14
+ useReducedMotion,
15
+ useHighContrast,
16
+ useFocusTrap,
17
+ useAnnounce,
18
+ useEscapeKey,
19
+ useClickOutside,
20
+ } from '../hooks';
21
+
22
+ // ── Form primitives ──────────────────────────────────────────────────────────
23
+ export { useTkxForm } from '../components/TkxForm';
24
+ export type { FormInstance, ValidationRule } from '../components/TkxForm';
25
+
26
+ // ── Engine utilities ─────────────────────────────────────────────────────────
27
+ export { extractAtomicCSS, resetAtomicCSS, cx, tkxPlugin, tkxRemovePlugin, tkxListPlugins } from '../engine/tkx';
28
+ export { extractCSS, resetStyles, injectStyles, cssVar } from '../engine/css';
29
+ export { meetsAA, meetsAAA, contrastRatio } from '../engine/wcag';
30
+ export { sanitizeString, sanitizeProps } from '../engine/security';
31
+
32
+ // ── Interaction hooks ────────────────────────────────────────────────────────
33
+ // Additional behavior primitives beyond the base hook set.
34
+
35
+ export { useDisclosure } from './useDisclosure';
36
+ export { useRovingTabIndex } from './useRovingTabIndex';
37
+ export { useFormState } from './useFormState';
38
+ export { useListSelection } from './useListSelection';
39
+ export { useDebounce } from './useDebounce';
40
+ export { useThrottle } from './useThrottle';
41
+ export { useControllable } from './useControllable';
42
+ export { useMediaQuery } from './useMediaQuery';
43
+ export { useLocalStorage } from './useLocalStorage';
44
+ export { useIntersectionObserver } from './useIntersectionObserver';
@@ -0,0 +1,40 @@
1
+ import { useState, useCallback, useRef } from 'react';
2
+
3
+ /**
4
+ * Unified controlled/uncontrolled state management.
5
+ * The component accepts both a controlled `value` + `onChange` pair
6
+ * OR works standalone with internal state when they're not provided.
7
+ *
8
+ * This is the same pattern used by Radix UI, Mantine, and Ant Design internally.
9
+ *
10
+ * @example — in a custom Select component:
11
+ * const [currentValue, setCurrentValue] = useControllable({
12
+ * value: props.value, // may be undefined (uncontrolled)
13
+ * onChange: props.onChange, // may be undefined
14
+ * defaultValue: '',
15
+ * });
16
+ */
17
+ export function useControllable<T>({
18
+ value,
19
+ onChange,
20
+ defaultValue,
21
+ }: {
22
+ value?: T;
23
+ onChange?: (value: T) => void;
24
+ defaultValue: T;
25
+ }): [T, (next: T) => void] {
26
+ const isControlled = value !== undefined;
27
+ const [internalValue, setInternalValue] = useState<T>(defaultValue);
28
+ const onChangeRef = useRef(onChange);
29
+ onChangeRef.current = onChange;
30
+
31
+ const setValue = useCallback(
32
+ (next: T) => {
33
+ if (!isControlled) setInternalValue(next);
34
+ onChangeRef.current?.(next);
35
+ },
36
+ [isControlled],
37
+ );
38
+
39
+ return [isControlled ? value! : internalValue, setValue];
40
+ }
@@ -0,0 +1,35 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+
3
+ /**
4
+ * Returns a debounced version of the value that only updates after the
5
+ * specified delay has passed without changes.
6
+ *
7
+ * @example
8
+ * const debouncedSearch = useDebounce(searchQuery, 300);
9
+ * useEffect(() => { fetchResults(debouncedSearch); }, [debouncedSearch]);
10
+ */
11
+ export function useDebounce<T>(value: T, delay: number): T {
12
+ const [debouncedValue, setDebouncedValue] = useState(value);
13
+ useEffect(() => {
14
+ const timer = setTimeout(() => setDebouncedValue(value), delay);
15
+ return () => clearTimeout(timer);
16
+ }, [value, delay]);
17
+ return debouncedValue;
18
+ }
19
+
20
+ /**
21
+ * Returns a debounced callback that only fires after the delay.
22
+ */
23
+ export function useDebouncedCallback<T extends (...args: any[]) => any>(
24
+ fn: T,
25
+ delay: number,
26
+ ): (...args: Parameters<T>) => void {
27
+ const fnRef = useRef(fn);
28
+ fnRef.current = fn;
29
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
30
+
31
+ return useCallback((...args: Parameters<T>) => {
32
+ if (timerRef.current) clearTimeout(timerRef.current);
33
+ timerRef.current = setTimeout(() => fnRef.current(...args), delay);
34
+ }, [delay]);
35
+ }
@@ -0,0 +1,28 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ export interface UseDisclosureReturn {
4
+ isOpen: boolean;
5
+ open: () => void;
6
+ close: () => void;
7
+ toggle: () => void;
8
+ }
9
+
10
+ /**
11
+ * Manages boolean open/close state for modals, drawers, popovers, dropdowns, etc.
12
+ *
13
+ * @example
14
+ * const { isOpen, open, close, toggle } = useDisclosure();
15
+ * return (
16
+ * <>
17
+ * <button onClick={open}>Open</button>
18
+ * {isOpen && <MyModal onClose={close} />}
19
+ * </>
20
+ * );
21
+ */
22
+ export function useDisclosure(initialState = false): UseDisclosureReturn {
23
+ const [isOpen, setIsOpen] = useState(initialState);
24
+ const open = useCallback(() => setIsOpen(true), []);
25
+ const close = useCallback(() => setIsOpen(false), []);
26
+ const toggle = useCallback(() => setIsOpen(v => !v), []);
27
+ return { isOpen, open, close, toggle };
28
+ }
@@ -0,0 +1,111 @@
1
+ import { useState, useCallback, useRef } from 'react';
2
+
3
+ export type FormFieldValue = string | number | boolean | string[] | null | undefined;
4
+
5
+ export interface FieldState {
6
+ value: FormFieldValue;
7
+ error: string | null;
8
+ touched: boolean;
9
+ dirty: boolean;
10
+ }
11
+
12
+ export interface UseFormStateOptions<T extends Record<string, FormFieldValue>> {
13
+ initialValues: T;
14
+ validate?: (values: T) => Partial<Record<keyof T, string>>;
15
+ }
16
+
17
+ export interface UseFormStateReturn<T extends Record<string, FormFieldValue>> {
18
+ values: T;
19
+ errors: Partial<Record<keyof T, string>>;
20
+ touched: Partial<Record<keyof T, boolean>>;
21
+ dirty: boolean;
22
+ isValid: boolean;
23
+ setValue: (name: keyof T, value: FormFieldValue) => void;
24
+ setValues: (values: Partial<T>) => void;
25
+ touchField: (name: keyof T) => void;
26
+ validate: () => boolean;
27
+ reset: () => void;
28
+ getFieldProps: (name: keyof T) => {
29
+ value: FormFieldValue;
30
+ onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => void;
31
+ onBlur: () => void;
32
+ 'aria-invalid': boolean | undefined;
33
+ 'aria-describedby': string | undefined;
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Headless form state manager — no components, no context, no validation library required.
39
+ * Works with any input elements.
40
+ *
41
+ * @example
42
+ * const { values, errors, getFieldProps, validate } = useFormState({
43
+ * initialValues: { email: '', password: '' },
44
+ * validate: ({ email }) => ({
45
+ * email: !email.includes('@') ? 'Invalid email' : undefined,
46
+ * }),
47
+ * });
48
+ */
49
+ export function useFormState<T extends Record<string, FormFieldValue>>({
50
+ initialValues,
51
+ validate: validateFn,
52
+ }: UseFormStateOptions<T>): UseFormStateReturn<T> {
53
+ const [values, setValuesState] = useState<T>({ ...initialValues });
54
+ const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
55
+ const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
56
+ const initialRef = useRef(initialValues);
57
+
58
+ const setValue = useCallback((name: keyof T, value: FormFieldValue) => {
59
+ setValuesState(prev => ({ ...prev, [name]: value }));
60
+ }, []);
61
+
62
+ const setValues = useCallback((partial: Partial<T>) => {
63
+ setValuesState(prev => ({ ...prev, ...partial }));
64
+ }, []);
65
+
66
+ const touchField = useCallback((name: keyof T) => {
67
+ setTouched(prev => ({ ...prev, [name]: true }));
68
+ }, []);
69
+
70
+ const validate = useCallback(() => {
71
+ if (!validateFn) return true;
72
+ const newErrors = validateFn(values);
73
+ const filtered = Object.fromEntries(
74
+ Object.entries(newErrors).filter(([, v]) => v != null),
75
+ ) as Partial<Record<keyof T, string>>;
76
+ setErrors(filtered);
77
+ // Touch all fields so errors show
78
+ const allTouched = Object.fromEntries(
79
+ Object.keys(values).map(k => [k, true]),
80
+ ) as Partial<Record<keyof T, boolean>>;
81
+ setTouched(allTouched);
82
+ return Object.keys(filtered).length === 0;
83
+ }, [values, validateFn]);
84
+
85
+ const reset = useCallback(() => {
86
+ setValuesState({ ...initialRef.current });
87
+ setErrors({});
88
+ setTouched({});
89
+ }, []);
90
+
91
+ const dirty = Object.keys(values).some(
92
+ k => values[k as keyof T] !== initialRef.current[k as keyof T],
93
+ );
94
+
95
+ const isValid = Object.keys(errors).length === 0;
96
+
97
+ const getFieldProps = useCallback(
98
+ (name: keyof T) => ({
99
+ value: values[name],
100
+ onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
101
+ setValue(name, e.target.value);
102
+ },
103
+ onBlur: () => touchField(name),
104
+ 'aria-invalid': touched[name] && !!errors[name] ? true : undefined,
105
+ 'aria-describedby': errors[name] ? `${String(name)}-error` : undefined,
106
+ }),
107
+ [values, errors, touched, setValue, touchField],
108
+ );
109
+
110
+ return { values, errors, touched, dirty, isValid, setValue, setValues, touchField, validate, reset, getFieldProps };
111
+ }
@@ -0,0 +1,51 @@
1
+ import { useState, useEffect, useRef, type RefObject } from 'react';
2
+
3
+ export interface UseIntersectionObserverOptions {
4
+ threshold?: number | number[];
5
+ rootMargin?: string;
6
+ root?: Element | null;
7
+ /** Unobserve after first intersection (useful for lazy-load). Default: false */
8
+ once?: boolean;
9
+ }
10
+
11
+ export interface UseIntersectionObserverReturn {
12
+ ref: RefObject<HTMLElement | null>;
13
+ isIntersecting: boolean;
14
+ entry: IntersectionObserverEntry | null;
15
+ }
16
+
17
+ /**
18
+ * Observe when an element enters or leaves the viewport.
19
+ * Perfect for lazy loading, infinite scroll, and scroll-triggered animations.
20
+ *
21
+ * @example
22
+ * const { ref, isIntersecting } = useIntersectionObserver({ threshold: 0.1 });
23
+ * return <div ref={ref} style={{ opacity: isIntersecting ? 1 : 0 }}>Animated content</div>;
24
+ */
25
+ export function useIntersectionObserver({
26
+ threshold = 0,
27
+ rootMargin = '0px',
28
+ root = null,
29
+ once = false,
30
+ }: UseIntersectionObserverOptions = {}): UseIntersectionObserverReturn {
31
+ const ref = useRef<HTMLElement>(null);
32
+ const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
33
+ const observerRef = useRef<IntersectionObserver | null>(null);
34
+
35
+ useEffect(() => {
36
+ const el = ref.current;
37
+ if (!el || typeof IntersectionObserver === 'undefined') return;
38
+
39
+ observerRef.current = new IntersectionObserver(
40
+ ([e]) => {
41
+ setEntry(e);
42
+ if (once && e.isIntersecting) observerRef.current?.unobserve(el);
43
+ },
44
+ { threshold, rootMargin, root },
45
+ );
46
+ observerRef.current.observe(el);
47
+ return () => observerRef.current?.disconnect();
48
+ }, [threshold, rootMargin, root, once]);
49
+
50
+ return { ref, isIntersecting: entry?.isIntersecting ?? false, entry };
51
+ }
@@ -0,0 +1,101 @@
1
+ import { useState, useCallback, useMemo } from 'react';
2
+
3
+ export interface UseListSelectionOptions {
4
+ items: string[];
5
+ initialSelected?: string[];
6
+ /** Allow selecting multiple items. Default: true */
7
+ multiple?: boolean;
8
+ }
9
+
10
+ export interface UseListSelectionReturn {
11
+ selected: Set<string>;
12
+ isSelected: (id: string) => boolean;
13
+ toggle: (id: string) => void;
14
+ select: (id: string) => void;
15
+ deselect: (id: string) => void;
16
+ selectAll: () => void;
17
+ deselectAll: () => void;
18
+ toggleAll: () => void;
19
+ allSelected: boolean;
20
+ someSelected: boolean;
21
+ selectedCount: number;
22
+ selectedArray: string[];
23
+ }
24
+
25
+ /**
26
+ * Manages selection state for lists, tables, and grids.
27
+ * Supports single and multi-select modes.
28
+ *
29
+ * @example
30
+ * const { isSelected, toggle, selectedArray } = useListSelection({ items: rows.map(r => r.id) });
31
+ */
32
+ export function useListSelection({
33
+ items,
34
+ initialSelected = [],
35
+ multiple = true,
36
+ }: UseListSelectionOptions): UseListSelectionReturn {
37
+ const [selected, setSelected] = useState<Set<string>>(() => new Set(initialSelected));
38
+
39
+ const toggle = useCallback((id: string) => {
40
+ setSelected(prev => {
41
+ const next = new Set(prev);
42
+ if (next.has(id)) {
43
+ next.delete(id);
44
+ } else {
45
+ if (!multiple) next.clear();
46
+ next.add(id);
47
+ }
48
+ return next;
49
+ });
50
+ }, [multiple]);
51
+
52
+ const select = useCallback((id: string) => {
53
+ setSelected(prev => {
54
+ if (prev.has(id)) return prev;
55
+ const next = multiple ? new Set(prev) : new Set<string>();
56
+ next.add(id);
57
+ return next;
58
+ });
59
+ }, [multiple]);
60
+
61
+ const deselect = useCallback((id: string) => {
62
+ setSelected(prev => {
63
+ if (!prev.has(id)) return prev;
64
+ const next = new Set(prev);
65
+ next.delete(id);
66
+ return next;
67
+ });
68
+ }, []);
69
+
70
+ const selectAll = useCallback(() => {
71
+ if (multiple) setSelected(new Set(items));
72
+ }, [items, multiple]);
73
+
74
+ const deselectAll = useCallback(() => setSelected(new Set()), []);
75
+
76
+ const toggleAll = useCallback(() => {
77
+ setSelected(prev => {
78
+ const allSel = items.every(id => prev.has(id));
79
+ return allSel ? new Set() : new Set(items);
80
+ });
81
+ }, [items]);
82
+
83
+ const allSelected = items.length > 0 && items.every(id => selected.has(id));
84
+ const someSelected = items.some(id => selected.has(id));
85
+ const selectedArray = useMemo(() => Array.from(selected), [selected]);
86
+
87
+ return {
88
+ selected,
89
+ isSelected: useCallback((id: string) => selected.has(id), [selected]),
90
+ toggle,
91
+ select,
92
+ deselect,
93
+ selectAll,
94
+ deselectAll,
95
+ toggleAll,
96
+ allSelected,
97
+ someSelected,
98
+ selectedCount: selected.size,
99
+ selectedArray,
100
+ };
101
+ }
@@ -0,0 +1,46 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ /**
4
+ * Persists state to localStorage. Falls back to in-memory on parse errors or
5
+ * SSR (where localStorage is unavailable).
6
+ *
7
+ * @example
8
+ * const [theme, setTheme] = useLocalStorage('theme', 'dark');
9
+ */
10
+ export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void, () => void] {
11
+ const [storedValue, setStoredValue] = useState<T>(() => {
12
+ if (typeof window === 'undefined') return initialValue;
13
+ try {
14
+ const item = window.localStorage.getItem(key);
15
+ return item !== null ? (JSON.parse(item) as T) : initialValue;
16
+ } catch {
17
+ return initialValue;
18
+ }
19
+ });
20
+
21
+ const setValue = useCallback(
22
+ (value: T | ((prev: T) => T)) => {
23
+ setStoredValue(prev => {
24
+ const next = typeof value === 'function' ? (value as (p: T) => T)(prev) : value;
25
+ if (typeof window !== 'undefined') {
26
+ try {
27
+ window.localStorage.setItem(key, JSON.stringify(next));
28
+ } catch {
29
+ // Quota exceeded or private browsing — silently ignore
30
+ }
31
+ }
32
+ return next;
33
+ });
34
+ },
35
+ [key],
36
+ );
37
+
38
+ const removeValue = useCallback(() => {
39
+ setStoredValue(initialValue);
40
+ if (typeof window !== 'undefined') {
41
+ try { window.localStorage.removeItem(key); } catch { /* ignore */ }
42
+ }
43
+ }, [key, initialValue]);
44
+
45
+ return [storedValue, setValue, removeValue];
46
+ }
@@ -0,0 +1,37 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ /**
4
+ * Reactive media query hook. Returns true while the query matches.
5
+ *
6
+ * @example
7
+ * const isMobile = useMediaQuery('(max-width: 767px)');
8
+ * const isDark = useMediaQuery('(prefers-color-scheme: dark)');
9
+ * const isTouch = useMediaQuery('(hover: none) and (pointer: coarse)');
10
+ */
11
+ export function useMediaQuery(query: string): boolean {
12
+ const [matches, setMatches] = useState(() => {
13
+ if (typeof window === 'undefined') return false;
14
+ return window.matchMedia(query).matches;
15
+ });
16
+
17
+ useEffect(() => {
18
+ if (typeof window === 'undefined') return;
19
+ const mq = window.matchMedia(query);
20
+ setMatches(mq.matches);
21
+ const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
22
+ mq.addEventListener('change', handler);
23
+ return () => mq.removeEventListener('change', handler);
24
+ }, [query]);
25
+
26
+ return matches;
27
+ }
28
+
29
+ /** Preset breakpoint hooks matching TekiVex's breakpoint scale. */
30
+ export function useBreakpoint() {
31
+ const sm = useMediaQuery('(min-width: 640px)');
32
+ const md = useMediaQuery('(min-width: 768px)');
33
+ const lg = useMediaQuery('(min-width: 1024px)');
34
+ const xl = useMediaQuery('(min-width: 1280px)');
35
+ const xxl = useMediaQuery('(min-width: 1536px)');
36
+ return { sm, md, lg, xl, xxl, isMobile: !sm, isTablet: sm && !lg, isDesktop: lg };
37
+ }