@salt-ds/styles 0.0.0-snapshot-20260109210611 → 0.0.0-snapshot-20260112210403

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/CHANGELOG.md CHANGED
@@ -1,10 +1,30 @@
1
1
  # @salt-ds/styles
2
2
 
3
- ## 0.0.0-snapshot-20260109210611
3
+ ## 0.0.0-snapshot-20260112210403
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - e12cacd: poc
7
+ - b639bf8: This feature is in-development, for exploration and feedback only.
8
+
9
+ Introduce a context‑driven API for injecting CSS class names into Salt components based on their props, and optionally stripping implementation‑only props before forwarding.
10
+
11
+ _Status_
12
+ Non‑breaking for existing Salt consumers
13
+ Experimental and incomplete — interfaces and behavior may change without notice
14
+
15
+ **Note to JPM employees**
16
+ Use only in non‑production codebases, or with prior permission from the Salt engineering team
17
+ What’s included
18
+
19
+ - `ClassNameInjectionProvider` — supplies a registry of class injectors via React context
20
+ - `useClassNameInjection(component, props)`
21
+ - computes additional classes via registered injectors
22
+ - merges them with any className provided at the call site
23
+ - removes internal/derived props (declared by each injector) before forwarding
24
+ - `registerClassInjector(registry, component, keys, injector)` — registers per‑component injection rules
25
+
26
+ _Documentation_
27
+ Full documentation will follow once the API is stabilized; for now, consider this API private and subject to change.
8
28
 
9
29
  ## 0.2.1
10
30
 
package/dist-cjs/index.js CHANGED
@@ -14,5 +14,5 @@ exports.useInsertionPoint = InsertionPointProvider.useInsertionPoint;
14
14
  exports.useComponentCssInjection = useStyleInjection.useComponentCssInjection;
15
15
  exports.ClassNameInjectionProvider = useClassNameInjection.ClassNameInjectionProvider;
16
16
  exports.registerClassInjector = useClassNameInjection.registerClassInjector;
17
- exports.useInjectedClassName = useClassNameInjection.useInjectedClassName;
17
+ exports.useClassNameInjection = useClassNameInjection.useClassNameInjection;
18
18
  //# sourceMappingURL=index.js.map
@@ -4,41 +4,72 @@ var jsxRuntime = require('react/jsx-runtime');
4
4
  var clsx = require('clsx');
5
5
  var React = require('react');
6
6
 
7
- const InjectionContext = React.createContext(null);
7
+ const EMPTY_REGISTRY = /* @__PURE__ */ new Map();
8
+ const InjectionContext = React.createContext(EMPTY_REGISTRY);
9
+ let hasWarnedExperimentalOnce = false;
8
10
  function ClassNameInjectionProvider({
9
11
  children,
10
12
  value
11
13
  }) {
12
- const registry = React.useMemo(() => value ?? {}, [value]);
14
+ const registry = React.useMemo(() => value ?? EMPTY_REGISTRY, [value]);
15
+ React.useEffect(() => {
16
+ if (!hasWarnedExperimentalOnce && process.env.NODE_ENV !== "production") {
17
+ console.warn(
18
+ "Salt ClassNameInjectionProvider is experimental and subject to change. JPM users: only recommended in non-production environments or with prior permission from the Salt team."
19
+ );
20
+ hasWarnedExperimentalOnce = true;
21
+ }
22
+ }, []);
13
23
  return /* @__PURE__ */ jsxRuntime.jsx(InjectionContext.Provider, { value: registry, children });
14
24
  }
15
- function useInjectedClassName(component, props) {
16
- const { className: classNameProp, ...restProps } = props;
25
+ function useClassNameInjection(component, props) {
17
26
  const registry = React.useContext(InjectionContext);
18
- if (!registry) {
19
- return {
20
- className: classNameProp || "",
21
- props: restProps
22
- };
23
- }
24
- const entries = registry[component] ?? [];
25
- const injected = entries.map((e) => e.fn(restProps)).filter(Boolean);
26
- const className = clsx.clsx(classNameProp, injected);
27
- const cleanProps = { ...restProps };
28
- for (const entry of entries) {
29
- for (const key of entry.keys) {
30
- delete cleanProps[key];
27
+ const entries = registry.get(component) ?? [];
28
+ const deps = React.useMemo(
29
+ () => entries.length ? entries.flatMap((e) => e.keys.map((k) => props[k])) : [],
30
+ [entries, props]
31
+ );
32
+ const injected = React.useMemo(() => {
33
+ const { className: _ignore, ...restProps } = props;
34
+ if (!entries.length) return [];
35
+ return entries.map((e) => e.fn(restProps)).filter((v) => v != null);
36
+ }, [entries, deps, props]);
37
+ const className = React.useMemo(
38
+ () => clsx.clsx(props.className, injected) || void 0,
39
+ [props, injected]
40
+ );
41
+ const cleanProps = React.useMemo(() => {
42
+ const { className: _ignore, ...restProps } = props;
43
+ if (!entries.length) {
44
+ return restProps;
31
45
  }
32
- }
46
+ const copy = { ...restProps };
47
+ for (const entry of entries) {
48
+ for (const key of entry.keys) {
49
+ if (Object.hasOwn(copy, key)) {
50
+ delete copy[key];
51
+ }
52
+ }
53
+ }
54
+ return copy;
55
+ }, [entries, deps, props]);
33
56
  return { className, props: cleanProps };
34
57
  }
35
58
  function registerClassInjector(registry, component, keys, injector) {
36
- registry[component] ??= [];
37
- const wrapped = (props) => injector(props);
38
- registry[component] = [...registry[component], { fn: wrapped, keys }];
59
+ if (!registry.has(component)) {
60
+ registry.set(component, []);
61
+ }
62
+ const wrapped = (props) => {
63
+ const picked = Object.fromEntries(keys.map((k) => [k, props[k]]));
64
+ return injector(picked);
65
+ };
66
+ registry.get(component).push({
67
+ fn: wrapped,
68
+ keys
69
+ });
39
70
  }
40
71
 
41
72
  exports.ClassNameInjectionProvider = ClassNameInjectionProvider;
42
73
  exports.registerClassInjector = registerClassInjector;
43
- exports.useInjectedClassName = useInjectedClassName;
74
+ exports.useClassNameInjection = useClassNameInjection;
44
75
  //# sourceMappingURL=useClassNameInjection.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useClassNameInjection.js","sources":["../src/useClassNameInjection.tsx"],"sourcesContent":["import { clsx } from \"clsx\";\nimport { createContext, useContext, useMemo } from \"react\";\n\nexport type ClassNameInjector<Props, Keys extends keyof Props> = (\n props: Pick<Props, Keys>,\n) => string | undefined;\n\ninterface ClassNameInjectorEntry<Props> {\n fn: (props: Props) => string | undefined;\n keys: (keyof Props)[];\n}\n\nexport type ClassNameInjectionRegistry = Record<\n string,\n // biome-ignore lint/suspicious/noExplicitAny: refer to ClassNameInjector which derives it's entry type based on the Props\n ClassNameInjectorEntry<any>[]\n>;\n\nconst InjectionContext = createContext<ClassNameInjectionRegistry | null>(null);\n\nexport type ClassNameInjectionProviderProps = {\n children: React.ReactNode;\n value?: ClassNameInjectionRegistry;\n};\n\nexport function ClassNameInjectionProvider({\n children,\n value,\n}: ClassNameInjectionProviderProps) {\n const registry = useMemo(() => value ?? {}, [value]);\n return (\n <InjectionContext.Provider value={registry}>\n {children}\n </InjectionContext.Provider>\n );\n}\n\ntype PropsWithClassName = { className?: string } & Record<string, any>;\n\nexport function useInjectedClassName<Props extends PropsWithClassName>(\n component: string,\n props: Props,\n): { className: string; props: Omit<Props, \"className\"> } {\n const { className: classNameProp, ...restProps } = props;\n const registry = useContext(InjectionContext);\n\n if (!registry) {\n return {\n className: classNameProp || \"\",\n props: restProps as Omit<Props, \"className\">,\n };\n }\n\n const entries = registry[component] ?? [];\n const injected = entries.map((e) => e.fn(restProps)).filter(Boolean);\n const className = clsx(classNameProp, injected);\n\n const cleanProps = { ...restProps } as Omit<Props, \"className\">;\n for (const entry of entries) {\n for (const key of entry.keys) {\n delete (cleanProps as any)[key];\n }\n }\n\n return { className, props: cleanProps };\n}\n\nexport function registerClassInjector<Props, Keys extends keyof Props>(\n registry: ClassNameInjectionRegistry,\n component: string,\n keys: Keys[],\n injector: ClassNameInjector<Props, Keys>,\n) {\n registry[component] ??= [];\n const wrapped = (props: Props) => injector(props as Props);\n registry[component] = [...registry[component], { fn: wrapped, keys }];\n}\n"],"names":["createContext","useMemo","useContext","clsx"],"mappings":";;;;;;AAkBA,MAAM,gBAAA,GAAmBA,oBAAiD,IAAI,CAAA;AAOvE,SAAS,0BAAA,CAA2B;AAAA,EACzC,QAAA;AAAA,EACA;AACF,CAAA,EAAoC;AAClC,EAAA,MAAM,QAAA,GAAWC,cAAQ,MAAM,KAAA,IAAS,EAAC,EAAG,CAAC,KAAK,CAAC,CAAA;AACnD,EAAA,sCACG,gBAAA,CAAiB,QAAA,EAAjB,EAA0B,KAAA,EAAO,UAC/B,QAAA,EACH,CAAA;AAEJ;AAIO,SAAS,oBAAA,CACd,WACA,KAAA,EACwD;AACxD,EAAA,MAAM,EAAE,SAAA,EAAW,aAAA,EAAe,GAAG,WAAU,GAAI,KAAA;AACnD,EAAA,MAAM,QAAA,GAAWC,iBAAW,gBAAgB,CAAA;AAE5C,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,OAAO;AAAA,MACL,WAAW,aAAA,IAAiB,EAAA;AAAA,MAC5B,KAAA,EAAO;AAAA,KACT;AAAA,EACF;AAEA,EAAA,MAAM,OAAA,GAAU,QAAA,CAAS,SAAS,CAAA,IAAK,EAAC;AACxC,EAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,CAAG,SAAS,CAAC,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA;AACnE,EAAA,MAAM,SAAA,GAAYC,SAAA,CAAK,aAAA,EAAe,QAAQ,CAAA;AAE9C,EAAA,MAAM,UAAA,GAAa,EAAE,GAAG,SAAA,EAAU;AAClC,EAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC3B,IAAA,KAAA,MAAW,GAAA,IAAO,MAAM,IAAA,EAAM;AAC5B,MAAA,OAAQ,WAAmB,GAAG,CAAA;AAAA,IAChC;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,SAAA,EAAW,KAAA,EAAO,UAAA,EAAW;AACxC;AAEO,SAAS,qBAAA,CACd,QAAA,EACA,SAAA,EACA,IAAA,EACA,QAAA,EACA;AACA,EAAA,QAAA,CAAS,SAAS,MAAM,EAAC;AACzB,EAAA,MAAM,OAAA,GAAU,CAAC,KAAA,KAAiB,QAAA,CAAS,KAAc,CAAA;AACzD,EAAA,QAAA,CAAS,SAAS,CAAA,GAAI,CAAC,GAAG,QAAA,CAAS,SAAS,CAAA,EAAG,EAAE,EAAA,EAAI,OAAA,EAAS,IAAA,EAAM,CAAA;AACtE;;;;;;"}
1
+ {"version":3,"file":"useClassNameInjection.js","sources":["../src/useClassNameInjection.tsx"],"sourcesContent":["import { clsx } from \"clsx\";\nimport { createContext, useContext, useEffect, useMemo } from \"react\";\n\n/**\n * Extensible map of supported components → their props.\n * @salt-ds/core must augment this with the components that support the API,\n * using the same key string that the component passes to useClassNameInjection,\n * e.g., \"saltButton\": ButtonProps.\n */\nexport type ComponentPropMap = {};\n\n/** Component key type: only keys declared in ComponentPropMap are allowed. */\ntype SupportedComponent = keyof ComponentPropMap extends never\n ? string\n : keyof ComponentPropMap;\n\nexport type ClassNameInjector<Props, Keys extends keyof Props> = (\n props: Pick<Props, Keys>,\n) => string | undefined;\n\ninterface ClassNameInjectorEntry {\n fn: (props: unknown) => string | undefined;\n keys: string[];\n}\n\nexport type ClassNameInjectionRegistry = Map<\n SupportedComponent,\n ClassNameInjectorEntry[]\n>;\n\nconst EMPTY_REGISTRY: ClassNameInjectionRegistry = new Map();\nconst InjectionContext =\n createContext<ClassNameInjectionRegistry>(EMPTY_REGISTRY);\n\nexport type ClassNameInjectionProviderProps = {\n children: React.ReactNode;\n value?: ClassNameInjectionRegistry;\n};\n\nlet hasWarnedExperimentalOnce = false;\n\nexport function ClassNameInjectionProvider({\n children,\n value,\n}: ClassNameInjectionProviderProps) {\n const registry = useMemo(() => value ?? EMPTY_REGISTRY, [value]);\n\n useEffect(() => {\n if (!hasWarnedExperimentalOnce && process.env.NODE_ENV !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n \"Salt ClassNameInjectionProvider is experimental and subject to change. JPM users: only recommended in non-production environments or with prior permission from the Salt team.\",\n );\n hasWarnedExperimentalOnce = true;\n }\n }, []);\n\n return (\n <InjectionContext.Provider value={registry}>\n {children}\n </InjectionContext.Provider>\n );\n}\n\ntype PropsWithClassName = { className?: string } & Record<string, any>;\n\n/**\n * Return the className created by the registry and a props object with injector keys stripped.\n * Only components declared in ComponentPropMap can call this at compile time.\n */\nexport function useClassNameInjection<Props extends PropsWithClassName>(\n component: SupportedComponent,\n props: Props,\n): { className: string | undefined; props: Omit<Props, \"className\"> } {\n const registry = useContext(InjectionContext);\n const entries = registry.get(component) ?? [];\n\n const deps = useMemo(\n () =>\n entries.length\n ? entries.flatMap((e) => e.keys.map((k) => (props as any)[k]))\n : [],\n [entries, props],\n );\n\n // Compute injected classes provided through ClassNameInjectionRegistry\n const injected = useMemo(() => {\n const { className: _ignore, ...restProps } = props as any;\n if (!entries.length) return [];\n return entries\n .map((e) => e.fn(restProps))\n .filter((v): v is string => v != null);\n }, [entries, deps, props]);\n\n // Merge original className with injected classes\n const className = useMemo(\n () => clsx((props as any).className, injected) || undefined,\n [props, injected],\n );\n\n // Create a cleaned props object by stripping keys used by injectors, to avoid DOM errors with unknown attributes\n const cleanProps = useMemo(() => {\n const { className: _ignore, ...restProps } = props as any;\n if (!entries.length) {\n return restProps as Omit<Props, \"className\">;\n }\n const copy = { ...restProps } as Omit<Props, \"className\">;\n for (const entry of entries) {\n for (const key of entry.keys) {\n if (Object.hasOwn(copy, key)) {\n delete (copy as any)[key];\n }\n }\n }\n return copy;\n }, [entries, deps, props]);\n\n return { className, props: cleanProps };\n}\n\n/**\n * Register a class injector for a supported component name.\n * - Keys must be valid string keys for that component’s props (as declared in ComponentPropMap).\n * - Injector receives only the declared keys (Pick<PropsOf<C>, Keys>).\n */\nexport function registerClassInjector<\n Props extends Record<string, any>,\n Keys extends Extract<keyof Props, string>,\n>(\n registry: ClassNameInjectionRegistry,\n component: SupportedComponent,\n keys: Keys[],\n injector: ClassNameInjector<Props, Keys>,\n) {\n if (!registry.has(component)) {\n registry.set(component, []);\n }\n\n const wrapped = (props: Record<string, any>) => {\n const picked = Object.fromEntries(keys.map((k) => [k, props[k]])) as Pick<\n Props,\n Keys\n >;\n return injector(picked);\n };\n\n registry.get(component)!.push({\n fn: wrapped as (props: unknown) => string | undefined,\n keys: keys as string[],\n });\n}\n"],"names":["createContext","useMemo","useEffect","useContext","clsx"],"mappings":"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"}
package/dist-es/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { StyleInjectionProvider, useStyleInjection } from './style-injection-provider/index.js';
2
2
  export { InsertionPointProvider, useInsertionPoint } from './use-style-injection/InsertionPointProvider.js';
3
3
  export { useComponentCssInjection } from './use-style-injection/useStyleInjection.js';
4
- export { ClassNameInjectionProvider, registerClassInjector, useInjectedClassName } from './useClassNameInjection.js';
4
+ export { ClassNameInjectionProvider, registerClassInjector, useClassNameInjection } from './useClassNameInjection.js';
5
5
  //# sourceMappingURL=index.js.map
@@ -1,40 +1,71 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import { clsx } from 'clsx';
3
- import { createContext, useMemo, useContext } from 'react';
3
+ import { createContext, useMemo, useEffect, useContext } from 'react';
4
4
 
5
- const InjectionContext = createContext(null);
5
+ const EMPTY_REGISTRY = /* @__PURE__ */ new Map();
6
+ const InjectionContext = createContext(EMPTY_REGISTRY);
7
+ let hasWarnedExperimentalOnce = false;
6
8
  function ClassNameInjectionProvider({
7
9
  children,
8
10
  value
9
11
  }) {
10
- const registry = useMemo(() => value ?? {}, [value]);
12
+ const registry = useMemo(() => value ?? EMPTY_REGISTRY, [value]);
13
+ useEffect(() => {
14
+ if (!hasWarnedExperimentalOnce && process.env.NODE_ENV !== "production") {
15
+ console.warn(
16
+ "Salt ClassNameInjectionProvider is experimental and subject to change. JPM users: only recommended in non-production environments or with prior permission from the Salt team."
17
+ );
18
+ hasWarnedExperimentalOnce = true;
19
+ }
20
+ }, []);
11
21
  return /* @__PURE__ */ jsx(InjectionContext.Provider, { value: registry, children });
12
22
  }
13
- function useInjectedClassName(component, props) {
14
- const { className: classNameProp, ...restProps } = props;
23
+ function useClassNameInjection(component, props) {
15
24
  const registry = useContext(InjectionContext);
16
- if (!registry) {
17
- return {
18
- className: classNameProp || "",
19
- props: restProps
20
- };
21
- }
22
- const entries = registry[component] ?? [];
23
- const injected = entries.map((e) => e.fn(restProps)).filter(Boolean);
24
- const className = clsx(classNameProp, injected);
25
- const cleanProps = { ...restProps };
26
- for (const entry of entries) {
27
- for (const key of entry.keys) {
28
- delete cleanProps[key];
25
+ const entries = registry.get(component) ?? [];
26
+ const deps = useMemo(
27
+ () => entries.length ? entries.flatMap((e) => e.keys.map((k) => props[k])) : [],
28
+ [entries, props]
29
+ );
30
+ const injected = useMemo(() => {
31
+ const { className: _ignore, ...restProps } = props;
32
+ if (!entries.length) return [];
33
+ return entries.map((e) => e.fn(restProps)).filter((v) => v != null);
34
+ }, [entries, deps, props]);
35
+ const className = useMemo(
36
+ () => clsx(props.className, injected) || void 0,
37
+ [props, injected]
38
+ );
39
+ const cleanProps = useMemo(() => {
40
+ const { className: _ignore, ...restProps } = props;
41
+ if (!entries.length) {
42
+ return restProps;
29
43
  }
30
- }
44
+ const copy = { ...restProps };
45
+ for (const entry of entries) {
46
+ for (const key of entry.keys) {
47
+ if (Object.hasOwn(copy, key)) {
48
+ delete copy[key];
49
+ }
50
+ }
51
+ }
52
+ return copy;
53
+ }, [entries, deps, props]);
31
54
  return { className, props: cleanProps };
32
55
  }
33
56
  function registerClassInjector(registry, component, keys, injector) {
34
- registry[component] ??= [];
35
- const wrapped = (props) => injector(props);
36
- registry[component] = [...registry[component], { fn: wrapped, keys }];
57
+ if (!registry.has(component)) {
58
+ registry.set(component, []);
59
+ }
60
+ const wrapped = (props) => {
61
+ const picked = Object.fromEntries(keys.map((k) => [k, props[k]]));
62
+ return injector(picked);
63
+ };
64
+ registry.get(component).push({
65
+ fn: wrapped,
66
+ keys
67
+ });
37
68
  }
38
69
 
39
- export { ClassNameInjectionProvider, registerClassInjector, useInjectedClassName };
70
+ export { ClassNameInjectionProvider, registerClassInjector, useClassNameInjection };
40
71
  //# sourceMappingURL=useClassNameInjection.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useClassNameInjection.js","sources":["../src/useClassNameInjection.tsx"],"sourcesContent":["import { clsx } from \"clsx\";\nimport { createContext, useContext, useMemo } from \"react\";\n\nexport type ClassNameInjector<Props, Keys extends keyof Props> = (\n props: Pick<Props, Keys>,\n) => string | undefined;\n\ninterface ClassNameInjectorEntry<Props> {\n fn: (props: Props) => string | undefined;\n keys: (keyof Props)[];\n}\n\nexport type ClassNameInjectionRegistry = Record<\n string,\n // biome-ignore lint/suspicious/noExplicitAny: refer to ClassNameInjector which derives it's entry type based on the Props\n ClassNameInjectorEntry<any>[]\n>;\n\nconst InjectionContext = createContext<ClassNameInjectionRegistry | null>(null);\n\nexport type ClassNameInjectionProviderProps = {\n children: React.ReactNode;\n value?: ClassNameInjectionRegistry;\n};\n\nexport function ClassNameInjectionProvider({\n children,\n value,\n}: ClassNameInjectionProviderProps) {\n const registry = useMemo(() => value ?? {}, [value]);\n return (\n <InjectionContext.Provider value={registry}>\n {children}\n </InjectionContext.Provider>\n );\n}\n\ntype PropsWithClassName = { className?: string } & Record<string, any>;\n\nexport function useInjectedClassName<Props extends PropsWithClassName>(\n component: string,\n props: Props,\n): { className: string; props: Omit<Props, \"className\"> } {\n const { className: classNameProp, ...restProps } = props;\n const registry = useContext(InjectionContext);\n\n if (!registry) {\n return {\n className: classNameProp || \"\",\n props: restProps as Omit<Props, \"className\">,\n };\n }\n\n const entries = registry[component] ?? [];\n const injected = entries.map((e) => e.fn(restProps)).filter(Boolean);\n const className = clsx(classNameProp, injected);\n\n const cleanProps = { ...restProps } as Omit<Props, \"className\">;\n for (const entry of entries) {\n for (const key of entry.keys) {\n delete (cleanProps as any)[key];\n }\n }\n\n return { className, props: cleanProps };\n}\n\nexport function registerClassInjector<Props, Keys extends keyof Props>(\n registry: ClassNameInjectionRegistry,\n component: string,\n keys: Keys[],\n injector: ClassNameInjector<Props, Keys>,\n) {\n registry[component] ??= [];\n const wrapped = (props: Props) => injector(props as Props);\n registry[component] = [...registry[component], { fn: wrapped, keys }];\n}\n"],"names":[],"mappings":";;;;AAkBA,MAAM,gBAAA,GAAmB,cAAiD,IAAI,CAAA;AAOvE,SAAS,0BAAA,CAA2B;AAAA,EACzC,QAAA;AAAA,EACA;AACF,CAAA,EAAoC;AAClC,EAAA,MAAM,QAAA,GAAW,QAAQ,MAAM,KAAA,IAAS,EAAC,EAAG,CAAC,KAAK,CAAC,CAAA;AACnD,EAAA,2BACG,gBAAA,CAAiB,QAAA,EAAjB,EAA0B,KAAA,EAAO,UAC/B,QAAA,EACH,CAAA;AAEJ;AAIO,SAAS,oBAAA,CACd,WACA,KAAA,EACwD;AACxD,EAAA,MAAM,EAAE,SAAA,EAAW,aAAA,EAAe,GAAG,WAAU,GAAI,KAAA;AACnD,EAAA,MAAM,QAAA,GAAW,WAAW,gBAAgB,CAAA;AAE5C,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,OAAO;AAAA,MACL,WAAW,aAAA,IAAiB,EAAA;AAAA,MAC5B,KAAA,EAAO;AAAA,KACT;AAAA,EACF;AAEA,EAAA,MAAM,OAAA,GAAU,QAAA,CAAS,SAAS,CAAA,IAAK,EAAC;AACxC,EAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,CAAG,SAAS,CAAC,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA;AACnE,EAAA,MAAM,SAAA,GAAY,IAAA,CAAK,aAAA,EAAe,QAAQ,CAAA;AAE9C,EAAA,MAAM,UAAA,GAAa,EAAE,GAAG,SAAA,EAAU;AAClC,EAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC3B,IAAA,KAAA,MAAW,GAAA,IAAO,MAAM,IAAA,EAAM;AAC5B,MAAA,OAAQ,WAAmB,GAAG,CAAA;AAAA,IAChC;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,SAAA,EAAW,KAAA,EAAO,UAAA,EAAW;AACxC;AAEO,SAAS,qBAAA,CACd,QAAA,EACA,SAAA,EACA,IAAA,EACA,QAAA,EACA;AACA,EAAA,QAAA,CAAS,SAAS,MAAM,EAAC;AACzB,EAAA,MAAM,OAAA,GAAU,CAAC,KAAA,KAAiB,QAAA,CAAS,KAAc,CAAA;AACzD,EAAA,QAAA,CAAS,SAAS,CAAA,GAAI,CAAC,GAAG,QAAA,CAAS,SAAS,CAAA,EAAG,EAAE,EAAA,EAAI,OAAA,EAAS,IAAA,EAAM,CAAA;AACtE;;;;"}
1
+ {"version":3,"file":"useClassNameInjection.js","sources":["../src/useClassNameInjection.tsx"],"sourcesContent":["import { clsx } from \"clsx\";\nimport { createContext, useContext, useEffect, useMemo } from \"react\";\n\n/**\n * Extensible map of supported components → their props.\n * @salt-ds/core must augment this with the components that support the API,\n * using the same key string that the component passes to useClassNameInjection,\n * e.g., \"saltButton\": ButtonProps.\n */\nexport type ComponentPropMap = {};\n\n/** Component key type: only keys declared in ComponentPropMap are allowed. */\ntype SupportedComponent = keyof ComponentPropMap extends never\n ? string\n : keyof ComponentPropMap;\n\nexport type ClassNameInjector<Props, Keys extends keyof Props> = (\n props: Pick<Props, Keys>,\n) => string | undefined;\n\ninterface ClassNameInjectorEntry {\n fn: (props: unknown) => string | undefined;\n keys: string[];\n}\n\nexport type ClassNameInjectionRegistry = Map<\n SupportedComponent,\n ClassNameInjectorEntry[]\n>;\n\nconst EMPTY_REGISTRY: ClassNameInjectionRegistry = new Map();\nconst InjectionContext =\n createContext<ClassNameInjectionRegistry>(EMPTY_REGISTRY);\n\nexport type ClassNameInjectionProviderProps = {\n children: React.ReactNode;\n value?: ClassNameInjectionRegistry;\n};\n\nlet hasWarnedExperimentalOnce = false;\n\nexport function ClassNameInjectionProvider({\n children,\n value,\n}: ClassNameInjectionProviderProps) {\n const registry = useMemo(() => value ?? EMPTY_REGISTRY, [value]);\n\n useEffect(() => {\n if (!hasWarnedExperimentalOnce && process.env.NODE_ENV !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n \"Salt ClassNameInjectionProvider is experimental and subject to change. JPM users: only recommended in non-production environments or with prior permission from the Salt team.\",\n );\n hasWarnedExperimentalOnce = true;\n }\n }, []);\n\n return (\n <InjectionContext.Provider value={registry}>\n {children}\n </InjectionContext.Provider>\n );\n}\n\ntype PropsWithClassName = { className?: string } & Record<string, any>;\n\n/**\n * Return the className created by the registry and a props object with injector keys stripped.\n * Only components declared in ComponentPropMap can call this at compile time.\n */\nexport function useClassNameInjection<Props extends PropsWithClassName>(\n component: SupportedComponent,\n props: Props,\n): { className: string | undefined; props: Omit<Props, \"className\"> } {\n const registry = useContext(InjectionContext);\n const entries = registry.get(component) ?? [];\n\n const deps = useMemo(\n () =>\n entries.length\n ? entries.flatMap((e) => e.keys.map((k) => (props as any)[k]))\n : [],\n [entries, props],\n );\n\n // Compute injected classes provided through ClassNameInjectionRegistry\n const injected = useMemo(() => {\n const { className: _ignore, ...restProps } = props as any;\n if (!entries.length) return [];\n return entries\n .map((e) => e.fn(restProps))\n .filter((v): v is string => v != null);\n }, [entries, deps, props]);\n\n // Merge original className with injected classes\n const className = useMemo(\n () => clsx((props as any).className, injected) || undefined,\n [props, injected],\n );\n\n // Create a cleaned props object by stripping keys used by injectors, to avoid DOM errors with unknown attributes\n const cleanProps = useMemo(() => {\n const { className: _ignore, ...restProps } = props as any;\n if (!entries.length) {\n return restProps as Omit<Props, \"className\">;\n }\n const copy = { ...restProps } as Omit<Props, \"className\">;\n for (const entry of entries) {\n for (const key of entry.keys) {\n if (Object.hasOwn(copy, key)) {\n delete (copy as any)[key];\n }\n }\n }\n return copy;\n }, [entries, deps, props]);\n\n return { className, props: cleanProps };\n}\n\n/**\n * Register a class injector for a supported component name.\n * - Keys must be valid string keys for that component’s props (as declared in ComponentPropMap).\n * - Injector receives only the declared keys (Pick<PropsOf<C>, Keys>).\n */\nexport function registerClassInjector<\n Props extends Record<string, any>,\n Keys extends Extract<keyof Props, string>,\n>(\n registry: ClassNameInjectionRegistry,\n component: SupportedComponent,\n keys: Keys[],\n injector: ClassNameInjector<Props, Keys>,\n) {\n if (!registry.has(component)) {\n registry.set(component, []);\n }\n\n const wrapped = (props: Record<string, any>) => {\n const picked = Object.fromEntries(keys.map((k) => [k, props[k]])) as Pick<\n Props,\n Keys\n >;\n return injector(picked);\n };\n\n registry.get(component)!.push({\n fn: wrapped as (props: unknown) => string | undefined,\n keys: keys as string[],\n });\n}\n"],"names":[],"mappings":"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"}
@@ -1,9 +1,18 @@
1
+ /**
2
+ * Extensible map of supported components → their props.
3
+ * @salt-ds/core must augment this with the components that support the API,
4
+ * using the same key string that the component passes to useClassNameInjection,
5
+ * e.g., "saltButton": ButtonProps.
6
+ */
7
+ export type ComponentPropMap = {};
8
+ /** Component key type: only keys declared in ComponentPropMap are allowed. */
9
+ type SupportedComponent = keyof ComponentPropMap extends never ? string : keyof ComponentPropMap;
1
10
  export type ClassNameInjector<Props, Keys extends keyof Props> = (props: Pick<Props, Keys>) => string | undefined;
2
- interface ClassNameInjectorEntry<Props> {
3
- fn: (props: Props) => string | undefined;
4
- keys: (keyof Props)[];
11
+ interface ClassNameInjectorEntry {
12
+ fn: (props: unknown) => string | undefined;
13
+ keys: string[];
5
14
  }
6
- export type ClassNameInjectionRegistry = Record<string, ClassNameInjectorEntry<any>[]>;
15
+ export type ClassNameInjectionRegistry = Map<SupportedComponent, ClassNameInjectorEntry[]>;
7
16
  export type ClassNameInjectionProviderProps = {
8
17
  children: React.ReactNode;
9
18
  value?: ClassNameInjectionRegistry;
@@ -12,9 +21,18 @@ export declare function ClassNameInjectionProvider({ children, value, }: ClassNa
12
21
  type PropsWithClassName = {
13
22
  className?: string;
14
23
  } & Record<string, any>;
15
- export declare function useInjectedClassName<Props extends PropsWithClassName>(component: string, props: Props): {
16
- className: string;
24
+ /**
25
+ * Return the className created by the registry and a props object with injector keys stripped.
26
+ * Only components declared in ComponentPropMap can call this at compile time.
27
+ */
28
+ export declare function useClassNameInjection<Props extends PropsWithClassName>(component: SupportedComponent, props: Props): {
29
+ className: string | undefined;
17
30
  props: Omit<Props, "className">;
18
31
  };
19
- export declare function registerClassInjector<Props, Keys extends keyof Props>(registry: ClassNameInjectionRegistry, component: string, keys: Keys[], injector: ClassNameInjector<Props, Keys>): void;
32
+ /**
33
+ * Register a class injector for a supported component name.
34
+ * - Keys must be valid string keys for that component’s props (as declared in ComponentPropMap).
35
+ * - Injector receives only the declared keys (Pick<PropsOf<C>, Keys>).
36
+ */
37
+ export declare function registerClassInjector<Props extends Record<string, any>, Keys extends Extract<keyof Props, string>>(registry: ClassNameInjectionRegistry, component: SupportedComponent, keys: Keys[], injector: ClassNameInjector<Props, Keys>): void;
20
38
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salt-ds/styles",
3
- "version": "0.0.0-snapshot-20260109210611",
3
+ "version": "0.0.0-snapshot-20260112210403",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",