@just-web/css 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/esm/class-name.d.ts +12 -0
  2. package/esm/class-name.d.ts.map +1 -0
  3. package/esm/class-name.js +6 -0
  4. package/esm/class-name.js.map +1 -0
  5. package/esm/css-properties.d.ts +10 -0
  6. package/esm/css-properties.d.ts.map +1 -0
  7. package/esm/css-properties.js +2 -0
  8. package/esm/css-properties.js.map +1 -0
  9. package/esm/globals.ctx.d.ts +6 -0
  10. package/esm/globals.ctx.d.ts.map +1 -0
  11. package/esm/globals.ctx.js +13 -0
  12. package/esm/globals.ctx.js.map +1 -0
  13. package/esm/index.d.ts +9 -0
  14. package/esm/index.d.ts.map +1 -0
  15. package/esm/index.js +9 -0
  16. package/esm/index.js.map +1 -0
  17. package/esm/style.d.ts +8 -0
  18. package/esm/style.d.ts.map +1 -0
  19. package/esm/style.js +2 -0
  20. package/esm/style.js.map +1 -0
  21. package/esm/testing/log-panel.d.ts +5 -0
  22. package/esm/testing/log-panel.d.ts.map +1 -0
  23. package/esm/testing/log-panel.js +5 -0
  24. package/esm/testing/log-panel.js.map +1 -0
  25. package/esm/testing/toggle-attribute-button.d.ts +5 -0
  26. package/esm/testing/toggle-attribute-button.d.ts.map +1 -0
  27. package/esm/testing/toggle-attribute-button.js +19 -0
  28. package/esm/testing/toggle-attribute-button.js.map +1 -0
  29. package/esm/theme/class-name.d.ts +42 -0
  30. package/esm/theme/class-name.d.ts.map +1 -0
  31. package/esm/theme/class-name.js +56 -0
  32. package/esm/theme/class-name.js.map +1 -0
  33. package/esm/theme/data-attribute.d.ts +68 -0
  34. package/esm/theme/data-attribute.d.ts.map +1 -0
  35. package/esm/theme/data-attribute.js +78 -0
  36. package/esm/theme/data-attribute.js.map +1 -0
  37. package/esm/utils/attribute.d.ts +37 -0
  38. package/esm/utils/attribute.d.ts.map +1 -0
  39. package/esm/utils/attribute.js +53 -0
  40. package/esm/utils/attribute.js.map +1 -0
  41. package/esm/utils/data-attribute.d.ts +24 -0
  42. package/esm/utils/data-attribute.d.ts.map +1 -0
  43. package/esm/utils/data-attribute.js +30 -0
  44. package/esm/utils/data-attribute.js.map +1 -0
  45. package/esm/utils/prefers-color-scheme.d.ts +34 -0
  46. package/esm/utils/prefers-color-scheme.d.ts.map +1 -0
  47. package/esm/utils/prefers-color-scheme.js +54 -0
  48. package/esm/utils/prefers-color-scheme.js.map +1 -0
  49. package/package.json +67 -0
  50. package/readme.md +15 -0
  51. package/src/class-name.ts +12 -0
  52. package/src/css-properties.ts +10 -0
  53. package/src/globals.ctx.ts +12 -0
  54. package/src/index.ts +8 -0
  55. package/src/style.ts +8 -0
  56. package/src/tailwind.css +1 -0
  57. package/src/testing/log-panel.tsx +12 -0
  58. package/src/testing/toggle-attribute-button.tsx +32 -0
  59. package/src/theme/class-name.ts +70 -0
  60. package/src/theme/data-attribute.ts +96 -0
  61. package/src/utils/attribute.ts +60 -0
  62. package/src/utils/data-attribute.ts +37 -0
  63. package/src/utils/prefers-color-scheme.ts +57 -0
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Observes system color scheme preference changes and calls handlers when they occur.
3
+ *
4
+ * @param themes - An object mapping theme names to handler functions that are called when that theme is activated
5
+ * @returns A cleanup function that removes all event listeners
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * // Observe light/dark mode changes
10
+ * const cleanup = observePrefersColorScheme({
11
+ * light: (theme) => console.log('Light mode activated'),
12
+ * dark: (theme) => console.log('Dark mode activated')
13
+ * })
14
+ *
15
+ * // Later, to stop observing:
16
+ * cleanup()
17
+ * ```
18
+ */
19
+ export declare function observePrefersColorScheme<T extends string>(themes: Record<T, (value: T | null) => void>): () => void;
20
+ /**
21
+ * Gets the current preferred color theme from the system settings.
22
+ *
23
+ * @param themes - A list of theme names to check against the system preference
24
+ * @returns The first matching theme from the provided list, or null if none match
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * // Check if system prefers light or dark mode
29
+ * const theme = getPrefersColorTheme('light', 'dark')
30
+ * // Returns 'light', 'dark', or null
31
+ * ```
32
+ */
33
+ export declare function getPrefersColorTheme<T extends string>(...themes: T[]): T | null;
34
+ //# sourceMappingURL=prefers-color-scheme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefers-color-scheme.d.ts","sourceRoot":"","sources":["../../src/utils/prefers-color-scheme.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC,cAkBvG;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,EAAE,YAEpE"}
@@ -0,0 +1,54 @@
1
+ import { mapKey } from 'type-plus';
2
+ import { ctx } from "../globals.ctx.js";
3
+ /**
4
+ * Observes system color scheme preference changes and calls handlers when they occur.
5
+ *
6
+ * @param themes - An object mapping theme names to handler functions that are called when that theme is activated
7
+ * @returns A cleanup function that removes all event listeners
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * // Observe light/dark mode changes
12
+ * const cleanup = observePrefersColorScheme({
13
+ * light: (theme) => console.log('Light mode activated'),
14
+ * dark: (theme) => console.log('Dark mode activated')
15
+ * })
16
+ *
17
+ * // Later, to stop observing:
18
+ * cleanup()
19
+ * ```
20
+ */
21
+ export function observePrefersColorScheme(themes) {
22
+ const removers = mapKey(themes, (t) => {
23
+ const m = ctx.matchMedia(`(prefers-color-scheme: ${t})`);
24
+ const listener = (event) => {
25
+ if (event.matches) {
26
+ themes[t]?.(t);
27
+ }
28
+ };
29
+ m.addEventListener('change', listener);
30
+ return () => m.removeEventListener('change', listener);
31
+ });
32
+ return () => {
33
+ for (const remover of removers) {
34
+ remover();
35
+ }
36
+ };
37
+ }
38
+ /**
39
+ * Gets the current preferred color theme from the system settings.
40
+ *
41
+ * @param themes - A list of theme names to check against the system preference
42
+ * @returns The first matching theme from the provided list, or null if none match
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * // Check if system prefers light or dark mode
47
+ * const theme = getPrefersColorTheme('light', 'dark')
48
+ * // Returns 'light', 'dark', or null
49
+ * ```
50
+ */
51
+ export function getPrefersColorTheme(...themes) {
52
+ return themes.find((theme) => ctx.matchMedia(`(prefers-color-scheme: ${theme})`).matches) ?? null;
53
+ }
54
+ //# sourceMappingURL=prefers-color-scheme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefers-color-scheme.js","sourceRoot":"","sources":["../../src/utils/prefers-color-scheme.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAClC,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAA;AAEvC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,yBAAyB,CAAmB,MAA4C;IACvG,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;QACrC,MAAM,CAAC,GAAG,GAAG,CAAC,UAAU,CAAC,0BAA0B,CAAW,GAAG,CAAC,CAAA;QAClE,MAAM,QAAQ,GAAG,CAAC,KAA0B,EAAE,EAAE;YAC/C,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;gBACnB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YACf,CAAC;QACF,CAAC,CAAA;QAED,CAAC,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;QACtC,OAAO,GAAG,EAAE,CAAC,CAAC,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;IACvD,CAAC,CAAC,CAAA;IAEF,OAAO,GAAG,EAAE;QACX,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAChC,OAAO,EAAE,CAAA;QACV,CAAC;IACF,CAAC,CAAA;AACF,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAmB,GAAG,MAAW;IACpE,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,0BAA0B,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,IAAI,CAAA;AAClG,CAAC"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@just-web/css",
3
+ "version": "0.1.0",
4
+ "description": "",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./esm/index.d.ts",
9
+ "default": "./esm/index.js"
10
+ },
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "esm",
15
+ "src",
16
+ "!**/*.{spec,test,unit,accept,integrate,system,stories}.*",
17
+ "!**/*.mdx"
18
+ ],
19
+ "dependencies": {
20
+ "csstype": "^3.1.3",
21
+ "type-plus": "8.0.0-beta.7"
22
+ },
23
+ "devDependencies": {
24
+ "@repobuddy/storybook": "^0.9.1",
25
+ "@repobuddy/vitest": "^1.2.2",
26
+ "@storybook/addon-essentials": "^8.6.12",
27
+ "@storybook/addon-storysource": "^8.6.12",
28
+ "@storybook/blocks": "^8.6.12",
29
+ "@storybook/experimental-addon-test": "^8.6.12",
30
+ "@storybook/manager-api": "^8.6.12",
31
+ "@storybook/preview-api": "^8.6.12",
32
+ "@storybook/react": "^8.6.12",
33
+ "@storybook/react-vite": "^8.6.12",
34
+ "@storybook/test": "^8.6.12",
35
+ "@storybook/theming": "^8.6.12",
36
+ "@tailwindcss/vite": "^4.1.6",
37
+ "@vitest/browser": "^3.1.3",
38
+ "@vitest/coverage-v8": "^3.1.3",
39
+ "dedent": "^1.6.0",
40
+ "playwright": "^1.52.0",
41
+ "react": "^18.3.1",
42
+ "react-dom": "^18.3.1",
43
+ "rimraf": "^6.0.1",
44
+ "storybook": "^8.6.12",
45
+ "storybook-addon-code-editor": "^4.1.1",
46
+ "storybook-addon-tag-badges": "^1.4.0",
47
+ "storybook-addon-vis": "^0.19.4",
48
+ "storybook-dark-mode": "^4.0.2",
49
+ "tailwindcss": "^4.1.6",
50
+ "typescript": "^5.8.3",
51
+ "vite": "^6.3.5",
52
+ "vitest": "^3.1.3",
53
+ "vitest-browser-react": "^0.1.1",
54
+ "@tools/typescript": "^0.0.1"
55
+ },
56
+ "scripts": {
57
+ "build": "run-p build:*",
58
+ "build-doc": "storybook build -o ../../docs/css",
59
+ "build:esm": "tsc -p tsconfig.esm.json",
60
+ "clean": "rimraf .turbo esm *.tsbuildinfo",
61
+ "cov": "vitest run --coverage",
62
+ "nuke": "rimraf node_modules",
63
+ "sb": "storybook dev -p 6206",
64
+ "test": "vitest run",
65
+ "w": "vitest"
66
+ }
67
+ }
package/readme.md ADDED
@@ -0,0 +1,15 @@
1
+ # @just-web/css
2
+
3
+ [@just-web/css][@just-web/css] provides CSS utilities and types for web applications.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @just-web/css
9
+
10
+ yarn add @just-web/css
11
+
12
+ pnpm add @just-web/css
13
+ ```
14
+
15
+ [@just-web/css]: https://github.com/justland/just-web-foundation/tree/main/libs/css
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Note that `className` could be specific to ReactJS.
3
+ * So this type may be misplaced in this package.
4
+ */
5
+
6
+ /**
7
+ * Interface for component props that include a className property.
8
+ * The className property accepts a string value for CSS class names.
9
+ */
10
+ export interface ClassNameProps {
11
+ className?: string | undefined
12
+ }
@@ -0,0 +1,10 @@
1
+ import type { Properties } from 'csstype'
2
+
3
+ /**
4
+ * Extends CSS properties to include custom properties.
5
+ * Allows for string or number values for standard properties,
6
+ * and string values for custom properties with '--' prefix.
7
+ */
8
+ export interface CSSProperties extends Properties<string | number> {
9
+ [k: `--${string}`]: string
10
+ }
@@ -0,0 +1,12 @@
1
+ export const ctx = {
2
+ matchMedia(query: string) {
3
+ return globalThis.matchMedia(query)
4
+ },
5
+ getDocumentElement() {
6
+ return globalThis.document.documentElement
7
+ },
8
+ _reset() {
9
+ this.matchMedia = globalThis.matchMedia
10
+ this.getDocumentElement = () => globalThis.document.documentElement
11
+ },
12
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './class-name.ts'
2
+ export * from './css-properties.ts'
3
+ export * from './style.ts'
4
+ export * from './theme/class-name.ts'
5
+ export * from './theme/data-attribute.ts'
6
+ export * from './utils/attribute.ts'
7
+ export * from './utils/data-attribute.ts'
8
+ export * from './utils/prefers-color-scheme.ts'
package/src/style.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { CSSProperties } from './css-properties.ts'
2
+
3
+ /**
4
+ * Interface for component props that include a style property.
5
+ */
6
+ export interface StyleProps {
7
+ style?: CSSProperties | undefined
8
+ }
@@ -0,0 +1 @@
1
+ @import "tailwindcss";
@@ -0,0 +1,12 @@
1
+ export function LogPanel({ title, log }: { title: string; log: string[] }) {
2
+ return (
3
+ <div className="bg-neutral-100 p-4 rounded overflow-y-auto">
4
+ <h4 className="mb-2">{title}</h4>
5
+ {log.map((entry, i) => (
6
+ <pre key={i} className="font-mono">
7
+ {entry}
8
+ </pre>
9
+ ))}
10
+ </div>
11
+ )
12
+ }
@@ -0,0 +1,32 @@
1
+ import { forwardRef, useCallback } from 'react'
2
+
3
+ export const ToggleAttributeButton = forwardRef<HTMLElement, { attribute: string; values?: string[] }>(
4
+ ({ attribute, values = ['test-value'] }, ref) => {
5
+ const handleAttributeChange = useCallback(
6
+ (attr: string) => {
7
+ // Handle both RefObject and function ref cases
8
+ const target = (ref && 'current' in ref ? ref.current : null) ?? document.documentElement
9
+ const currentValue = target.getAttribute(attr)
10
+ const nextIndex = currentValue ? values.indexOf(currentValue) + 1 : 0
11
+ const newValue = nextIndex < values.length ? values[nextIndex]! : null
12
+
13
+ if (newValue === null) {
14
+ target.removeAttribute(attr)
15
+ } else {
16
+ target.setAttribute(attr, newValue)
17
+ }
18
+ },
19
+ [ref, values],
20
+ )
21
+
22
+ return (
23
+ <button
24
+ key={attribute}
25
+ className="bg-cyan-700 text-white px-4 py-2 rounded-md shadow-md active:bg-cyan-800"
26
+ onClick={() => handleAttributeChange(attribute)}
27
+ >
28
+ Toggle {attribute}
29
+ </button>
30
+ )
31
+ },
32
+ )
@@ -0,0 +1,70 @@
1
+ import { findKey } from 'type-plus'
2
+ import { ctx } from '../globals.ctx.ts'
3
+ import { observeAttributes } from '../utils/attribute.ts'
4
+
5
+ /**
6
+ * Gets the current theme by checking element class names against a themes map.
7
+ *
8
+ * @param options - Configuration options
9
+ * @param options.themes - Record mapping theme keys to their class name values
10
+ * @param options.defaultTheme - Fallback theme key if no matching class is found
11
+ * @param options.element - Element to check classes on (defaults to document.documentElement)
12
+ * @returns The matching theme key or defaultTheme if no match found
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const themes = {
17
+ * light: 'theme-light',
18
+ * dark: 'theme-dark'
19
+ * }
20
+ *
21
+ * // Get current theme from document.documentElement
22
+ * const theme = getThemeByClassName({
23
+ * themes,
24
+ * defaultTheme: 'light'
25
+ * })
26
+ *
27
+ * // Get theme from specific element
28
+ * const theme = getThemeByClassName({
29
+ * themes,
30
+ * element: myElement,
31
+ * defaultTheme: 'light'
32
+ * })
33
+ * ```
34
+ */
35
+ export function getThemeByClassName<Themes extends Record<string, string>>(options: {
36
+ themes: Themes
37
+ defaultTheme?: keyof Themes | undefined
38
+ element?: Element | undefined
39
+ }): keyof Themes | undefined {
40
+ const element = options.element ?? ctx.getDocumentElement()
41
+ const className = element.className
42
+ const theme = findKey(options.themes, (theme) => className.includes(options.themes[theme]!))
43
+ return theme ?? options.defaultTheme
44
+ }
45
+
46
+ export function observeThemeByClassName<Themes extends Record<string, string>>(options: {
47
+ themes: Themes
48
+ handler: (value: string | undefined) => void
49
+ defaultTheme?: keyof Themes | undefined
50
+ element?: Element | undefined
51
+ }) {
52
+ return observeAttributes(
53
+ {
54
+ class: (value: string | null) => {
55
+ if (value === null) {
56
+ options.handler(options.defaultTheme as string)
57
+ return
58
+ }
59
+
60
+ for (const name in options.themes) {
61
+ if (value.includes(options.themes[name]!)) {
62
+ options.handler(name)
63
+ break
64
+ }
65
+ }
66
+ },
67
+ },
68
+ options.element,
69
+ )
70
+ }
@@ -0,0 +1,96 @@
1
+ import { findKey } from 'type-plus'
2
+ import { getDataAttribute, observeDataAttributes } from '../utils/data-attribute.ts'
3
+
4
+ /**
5
+ * Gets the theme based on a data attribute value.
6
+ *
7
+ * @param options - Configuration options
8
+ * @param options.themes - Record mapping theme keys to their data attribute values
9
+ * @param options.defaultTheme - Fallback theme key if attribute value doesn't match any theme
10
+ * @param options.attributeName - Name of the data attribute to check (must start with 'data-')
11
+ * @returns The matching theme key, or defaultTheme if no match found
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const themes = {
16
+ * light: 'light',
17
+ * dark: 'dark',
18
+ * system: 'system'
19
+ * }
20
+ *
21
+ * // Get theme from data-theme attribute
22
+ * const theme = getThemeByDataAttribute({
23
+ * themes,
24
+ * defaultTheme: 'system',
25
+ * attributeName: 'data-theme'
26
+ * })
27
+ * ```
28
+ */
29
+ export function getThemeByDataAttribute<Themes extends Record<string, string>>(options: {
30
+ themes: Themes
31
+ defaultTheme?: keyof Themes | undefined
32
+ attributeName: `data-${string}`
33
+ element?: Element | undefined
34
+ }): keyof Themes | undefined {
35
+ const value = getDataAttribute(options.attributeName, options.element)
36
+
37
+ const theme = findKey(options.themes, (theme) => options.themes[theme] === value)
38
+
39
+ return theme ?? options.defaultTheme
40
+ }
41
+
42
+ /**
43
+ * Observes changes to a theme data attribute and calls a handler when it changes.
44
+ *
45
+ * @param options - Configuration options
46
+ * @param options.themes - Record mapping theme keys to their data attribute values
47
+ * @param options.handler - Callback function called with the new theme value or null when removed
48
+ * @param options.defaultTheme - Fallback theme key if attribute value doesn't match any theme
49
+ * @param options.attributeName - Name of the data attribute to observe (must start with 'data-')
50
+ * @returns A MutationObserver that can be disconnected to stop observing
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * const themes = {
55
+ * light: 'light',
56
+ * dark: 'dark'
57
+ * }
58
+ *
59
+ * // Observe data-theme attribute changes
60
+ * const observer = observeThemeByDataAttributes({
61
+ * themes,
62
+ * handler: (theme) => console.log('Theme changed to:', theme),
63
+ * defaultTheme: 'light',
64
+ * attributeName: 'data-theme'
65
+ * })
66
+ *
67
+ * // Stop observing
68
+ * observer.disconnect()
69
+ * ```
70
+ */
71
+ export function observeThemeByDataAttributes<Themes extends Record<string, string>>(options: {
72
+ attributeName: `data-${string}`
73
+ themes: Themes
74
+ handler: (value: string | null) => void
75
+ defaultTheme?: keyof Themes | undefined
76
+ element?: Element | undefined
77
+ }) {
78
+ return observeDataAttributes(
79
+ {
80
+ [options.attributeName]: (value: string | null) => {
81
+ if (value === null) {
82
+ options.handler((options.defaultTheme as string) ?? null)
83
+ return
84
+ }
85
+
86
+ for (const name in options.themes) {
87
+ if (options.themes[name] === value) {
88
+ options.handler(name)
89
+ break
90
+ }
91
+ }
92
+ },
93
+ },
94
+ options.element,
95
+ )
96
+ }
@@ -0,0 +1,60 @@
1
+ import { ctx } from '../globals.ctx.ts'
2
+
3
+ /**
4
+ * Gets the value of an attribute from an element.
5
+ *
6
+ * @param qualifiedName - The name of the attribute to get
7
+ * @param element - The element to get the attribute from. Defaults to `document.documentElement`
8
+ * @returns The attribute value cast to type T, or null if the attribute doesn't exist
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // Get theme from document root
13
+ * const theme = getAttribute('data-theme')
14
+ *
15
+ * // Get data-testid from a specific element
16
+ * const testId = getAttribute('data-testid', element)
17
+ * ```
18
+ */
19
+ export function getAttribute<T extends string>(
20
+ qualifiedName: T,
21
+ element: Element | undefined = ctx.getDocumentElement(),
22
+ ) {
23
+ return element?.getAttribute(qualifiedName) as T | null
24
+ }
25
+
26
+ /**
27
+ * Observes attributes changes on an element and calls corresponding handlers.
28
+ *
29
+ * @param handlers - An object mapping attribute names to handler functions.
30
+ * @param element - The element to observe. Defaults to `document.documentElement`.
31
+ * @returns {MutationObserver} The observer instance, which can be used to disconnect the observer.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const observer = observeAttributes({
36
+ * 'data-theme': (attr, value) => console.log(`Theme changed to: ${value}`),
37
+ * 'class': (attr, value) => console.log(`class changed to: ${value}`)
38
+ * });
39
+ *
40
+ * // Later, to stop observing:
41
+ * observer.disconnect();
42
+ * ```
43
+ */
44
+ export function observeAttributes<T extends string>(
45
+ handlers: Record<string, (value: T | null) => void>,
46
+ element: Element | undefined = ctx.getDocumentElement(),
47
+ ) {
48
+ const observer = new MutationObserver((mutations) => {
49
+ for (const mutation of mutations) {
50
+ const attribute = mutation.attributeName!
51
+ const value = element.getAttribute(attribute) as T | null
52
+ handlers[attribute]?.(value)
53
+ }
54
+ })
55
+ observer.observe(element, {
56
+ attributes: true,
57
+ attributeFilter: Object.keys(handlers),
58
+ })
59
+ return observer
60
+ }
@@ -0,0 +1,37 @@
1
+ import { ctx } from '../globals.ctx.ts'
2
+ import { getAttribute, observeAttributes } from './attribute.ts'
3
+
4
+ export function getDataAttribute<T extends `data-${string}`>(
5
+ qualifiedName: T,
6
+ element: Element | undefined = ctx.getDocumentElement(),
7
+ ) {
8
+ return getAttribute(qualifiedName, element)
9
+ }
10
+
11
+ /**
12
+ * Observes changes to `data-*` attributes on an element and calls corresponding handlers.
13
+ *
14
+ * @param options - Configuration options
15
+ * @param options.handlers - An object mapping `data-*` attribute names to handler functions.
16
+ * @param options.element - The element to observe. Defaults to `document.documentElement`
17
+ * @returns {MutationObserver} The observer instance, which can be used to disconnect the observer
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const observer = observeDataAttributes({
22
+ * handlers: {
23
+ * 'data-theme': (value) => console.log(`Theme changed to: ${value}`),
24
+ * 'data-mode': (value) => console.log(`Mode changed to: ${value}`)
25
+ * }
26
+ * });
27
+ *
28
+ * // Later, to stop observing:
29
+ * observer.disconnect();
30
+ * ```
31
+ */
32
+ export function observeDataAttributes<T extends string, K extends `data-${string}`>(
33
+ handlers: Record<K, (value: T | null) => void>,
34
+ element?: Element | undefined,
35
+ ) {
36
+ return observeAttributes(handlers, element)
37
+ }
@@ -0,0 +1,57 @@
1
+ import { mapKey } from 'type-plus'
2
+ import { ctx } from '../globals.ctx.ts'
3
+
4
+ /**
5
+ * Observes system color scheme preference changes and calls handlers when they occur.
6
+ *
7
+ * @param themes - An object mapping theme names to handler functions that are called when that theme is activated
8
+ * @returns A cleanup function that removes all event listeners
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // Observe light/dark mode changes
13
+ * const cleanup = observePrefersColorScheme({
14
+ * light: (theme) => console.log('Light mode activated'),
15
+ * dark: (theme) => console.log('Dark mode activated')
16
+ * })
17
+ *
18
+ * // Later, to stop observing:
19
+ * cleanup()
20
+ * ```
21
+ */
22
+ export function observePrefersColorScheme<T extends string>(themes: Record<T, (value: T | null) => void>) {
23
+ const removers = mapKey(themes, (t) => {
24
+ const m = ctx.matchMedia(`(prefers-color-scheme: ${t as string})`)
25
+ const listener = (event: MediaQueryListEvent) => {
26
+ if (event.matches) {
27
+ themes[t]?.(t)
28
+ }
29
+ }
30
+
31
+ m.addEventListener('change', listener)
32
+ return () => m.removeEventListener('change', listener)
33
+ })
34
+
35
+ return () => {
36
+ for (const remover of removers) {
37
+ remover()
38
+ }
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Gets the current preferred color theme from the system settings.
44
+ *
45
+ * @param themes - A list of theme names to check against the system preference
46
+ * @returns The first matching theme from the provided list, or null if none match
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * // Check if system prefers light or dark mode
51
+ * const theme = getPrefersColorTheme('light', 'dark')
52
+ * // Returns 'light', 'dark', or null
53
+ * ```
54
+ */
55
+ export function getPrefersColorTheme<T extends string>(...themes: T[]) {
56
+ return themes.find((theme) => ctx.matchMedia(`(prefers-color-scheme: ${theme})`).matches) ?? null
57
+ }