darkify-js 1.1.12 → 1.1.13-beta.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.
package/dist/darkify.d.ts CHANGED
@@ -1,39 +1,68 @@
1
+ interface DarkifyPlugin {
2
+ el?: HTMLElement | ShadowRoot;
3
+ render(): void | HTMLElement | ShadowRoot;
4
+ onThemeChange?: (theme: string) => void;
5
+ onDestroy?: () => void;
6
+ }
7
+
8
+ interface DarkifyPluginElement {
9
+ new (host: any, options?: object): DarkifyPlugin;
10
+ pluginId: string;
11
+ }
12
+
13
+ type StorageType = 'local' | 'session' | 'none';
14
+
1
15
  interface Options {
2
16
  autoMatchTheme: boolean;
3
- useLocalStorage: boolean;
4
- useSessionStorage: boolean;
5
17
  useColorScheme: [string, string?];
18
+ useStorage: StorageType;
19
+ usePlugins?: (DarkifyPluginElement | [DarkifyPluginElement, any])[];
6
20
  }
7
21
 
8
22
  declare class Darkify {
9
23
  private static readonly storageKey;
10
24
  readonly options: Options;
25
+ private plugins;
11
26
  theme: string;
12
- _style: HTMLStyleElement;
13
- _meta: HTMLMetaElement;
27
+ private _elm;
28
+ private _meta;
29
+ private _style;
14
30
  /**
15
- * @param {string} element Button ID ( recommended ) or HTML element
16
- * @param {object} options Options
31
+ * Creates a new Darkify instance for managing dark/light theme
32
+ * @param element - Button ID (recommended) or HTML element selector
33
+ * @param options - Configuration options for customizing behavior
17
34
  * @see {@link https://github.com/emrocode/darkify-js/wiki|Documentation}
18
35
  */
19
36
  constructor(element: string, options: Partial<Options>);
20
37
  private init;
38
+ private initPlugins;
39
+ private notifyPlugins;
40
+ private getStorage;
21
41
  private getOsPreference;
22
42
  private createAttribute;
23
43
  private updateTags;
24
44
  private savePreference;
25
45
  private syncThemeBetweenTabs;
46
+ private setTheme;
26
47
  /**
27
48
  * Toggles the theme between light and dark modes
28
- * @returns {void}
29
49
  */
30
50
  toggleTheme(): void;
31
51
  /**
32
- * Retrieves the current active theme
33
- * @returns {string}
52
+ * Retrieves the currently active theme
53
+ * @returns The current theme name ('light' or 'dark')
34
54
  */
35
55
  getCurrentTheme(): string;
56
+ /**
57
+ * Destroys the Darkify instance and cleans up all resources
58
+ *
59
+ * Removes all event listeners (system theme changes, click handlers, storage events),
60
+ * destroys all active plugins, removes injected DOM elements (<style> and <meta> tags),
61
+ * and frees associated resources.
62
+ * Call this method when the instance is no longer needed to prevent memory leaks.
63
+ */
64
+ destroy(): void;
36
65
  }
37
66
 
38
67
  export { Darkify as default };
39
- export type { Options };
68
+ export type { DarkifyPlugin, Options };
@@ -1,30 +1,44 @@
1
1
  /**
2
2
  *
3
3
  * @author Emilio Romero <emrocode@gmail.com>
4
- * @version 1.1.12
4
+ * @version 1.1.13-beta.0
5
5
  * @license MIT
6
6
  */
7
- const isBrowser = typeof window !== 'undefined';
7
+ class EventListenerManager {
8
+ constructor() {
9
+ this.listeners = [];
10
+ }
11
+ addListener(target, event, handler, options) {
12
+ target.addEventListener(event, handler, options);
13
+ this.listeners.push({ target, event, handler, options });
14
+ }
15
+ clearListeners() {
16
+ this.listeners.forEach(({ target, event, handler, options }) => {
17
+ target.removeEventListener(event, handler, options);
18
+ });
19
+ this.listeners = [];
20
+ }
21
+ }
8
22
 
9
23
  const defaultOptions = {
10
24
  autoMatchTheme: true,
11
- useLocalStorage: true,
12
- useSessionStorage: false,
13
25
  useColorScheme: ['#ffffff', '#000000'],
26
+ useStorage: 'local',
14
27
  };
15
28
 
29
+ const isBrowser = typeof window !== 'undefined';
30
+
16
31
  class Darkify {
17
32
  constructor(element, options) {
18
33
  this.options = defaultOptions;
34
+ this.plugins = [];
19
35
  this.theme = 'light';
20
36
  if (!isBrowser)
21
37
  return;
38
+ this._elm = new EventListenerManager();
22
39
  const opts = Object.assign(Object.assign({}, defaultOptions), options);
23
- if (opts.useLocalStorage && opts.useSessionStorage) {
24
- opts.useSessionStorage = false;
25
- }
26
40
  this.options = opts;
27
- this.theme = this.getOsPreference(this.options);
41
+ this.theme = this.getOsPreference();
28
42
  this._style = document.createElement('style');
29
43
  this._meta = document.createElement('meta');
30
44
  this.init(element);
@@ -32,30 +46,59 @@ class Darkify {
32
46
  this.syncThemeBetweenTabs();
33
47
  }
34
48
  init(element) {
35
- window
36
- .matchMedia('(prefers-color-scheme: dark)')
37
- .addEventListener('change', ({ matches: isDark }) => {
49
+ this._elm.addListener(window.matchMedia('(prefers-color-scheme: dark)'), 'change', ({ matches: isDark }) => {
38
50
  this.theme = isDark ? 'dark' : 'light';
39
51
  this.createAttribute();
40
52
  });
41
- if (element) {
42
- document.addEventListener('DOMContentLoaded', () => {
53
+ this.initPlugins();
54
+ const hasWidget = this.plugins.some(p => p.el !== undefined);
55
+ if (element && !hasWidget) {
56
+ this._elm.addListener(document, 'DOMContentLoaded', () => {
43
57
  const htmlElement = document.querySelector(element);
44
- htmlElement === null || htmlElement === void 0 ? void 0 : htmlElement.addEventListener('click', () => this.toggleTheme());
58
+ if (htmlElement) {
59
+ this._elm.addListener(htmlElement, 'click', () => this.toggleTheme());
60
+ }
45
61
  });
46
62
  }
47
63
  }
48
- getOsPreference(options) {
49
- const { autoMatchTheme, useLocalStorage, useSessionStorage } = options;
50
- const STO = (useLocalStorage && window.localStorage.getItem(Darkify.storageKey)) ||
51
- (useSessionStorage && window.sessionStorage.getItem(Darkify.storageKey)) ||
52
- (autoMatchTheme && window.matchMedia('(prefers-color-scheme: dark)').matches
53
- ? 'dark'
54
- : 'light');
55
- return STO;
64
+ initPlugins() {
65
+ var _a;
66
+ (_a = this.options.usePlugins) === null || _a === void 0 ? void 0 : _a.forEach(p => {
67
+ const [Plugin, pluginOptions] = Array.isArray(p) ? p : [p, undefined];
68
+ const plugin = new Plugin(this, pluginOptions);
69
+ const renderedNode = plugin.render();
70
+ if (renderedNode instanceof HTMLElement || renderedNode instanceof ShadowRoot) {
71
+ plugin.el = renderedNode;
72
+ }
73
+ this.plugins.push(plugin);
74
+ });
75
+ }
76
+ notifyPlugins(theme) {
77
+ this.plugins.forEach(plugin => {
78
+ var _a;
79
+ (_a = plugin.onThemeChange) === null || _a === void 0 ? void 0 : _a.call(plugin, theme);
80
+ });
81
+ }
82
+ getStorage() {
83
+ const { useStorage } = this.options;
84
+ if (useStorage === 'none')
85
+ return;
86
+ return useStorage === 'local' ? window.localStorage : window.sessionStorage;
87
+ }
88
+ getOsPreference() {
89
+ const storage = this.getStorage();
90
+ if (storage) {
91
+ const stored = storage.getItem(Darkify.storageKey);
92
+ if (stored)
93
+ return stored;
94
+ }
95
+ if (this.options.autoMatchTheme) {
96
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
97
+ }
98
+ return 'light';
56
99
  }
57
100
  createAttribute() {
58
- const dataTheme = document.getElementsByTagName('html')[0];
101
+ const dataTheme = document.documentElement;
59
102
  const { useColorScheme } = this.options;
60
103
  const css = `/**! Darkify / A simple dark mode toggle library **/\n:root:where([data-theme="${this.theme}"]),[data-theme="${this.theme}"]{color-scheme:${this.theme}}`;
61
104
  dataTheme.dataset.theme = this.theme;
@@ -65,36 +108,61 @@ class Darkify {
65
108
  updateTags(css, useColorScheme) {
66
109
  const [lightColor, darkColor] = useColorScheme;
67
110
  this._meta.name = 'theme-color';
111
+ this._meta.media = `(prefers-color-scheme: ${this.theme})`;
68
112
  this._meta.content = this.theme === 'light' ? lightColor : (darkColor !== null && darkColor !== void 0 ? darkColor : lightColor);
69
113
  this._style.innerHTML = css;
70
- const head = document.getElementsByTagName('head')[0];
114
+ const head = document.head;
71
115
  if (!this._meta.parentNode)
72
116
  head.appendChild(this._meta);
73
117
  if (!this._style.parentNode)
74
118
  head.appendChild(this._style);
75
119
  }
76
120
  savePreference() {
77
- const { useLocalStorage } = this.options;
78
- const STO = useLocalStorage ? window.localStorage : window.sessionStorage;
79
- const OTS = useLocalStorage ? window.sessionStorage : window.localStorage;
121
+ const { useStorage } = this.options;
122
+ if (useStorage === 'none')
123
+ return;
124
+ const storage = useStorage === 'local';
125
+ const STO = storage ? window.localStorage : window.sessionStorage;
126
+ const OTS = storage ? window.sessionStorage : window.localStorage;
80
127
  OTS.removeItem(Darkify.storageKey);
81
128
  STO.setItem(Darkify.storageKey, this.theme);
82
129
  }
83
130
  syncThemeBetweenTabs() {
84
- window.addEventListener('storage', e => {
131
+ this._elm.addListener(window, 'storage', (e) => {
85
132
  if (e.key === Darkify.storageKey && e.newValue) {
86
133
  this.theme = e.newValue;
87
134
  this.createAttribute();
135
+ this.notifyPlugins(e.newValue);
88
136
  }
89
137
  });
90
138
  }
91
- toggleTheme() {
92
- this.theme = this.theme === 'light' ? 'dark' : 'light';
139
+ setTheme(newTheme) {
140
+ this.theme = newTheme;
93
141
  this.createAttribute();
142
+ this.notifyPlugins(newTheme);
143
+ }
144
+ toggleTheme() {
145
+ this.setTheme(this.theme === 'light' ? 'dark' : 'light');
94
146
  }
95
147
  getCurrentTheme() {
96
148
  return this.theme;
97
149
  }
150
+ destroy() {
151
+ var _a, _b, _c, _d;
152
+ this._elm.clearListeners();
153
+ (_b = (_a = this._style) === null || _a === void 0 ? void 0 : _a.parentNode) === null || _b === void 0 ? void 0 : _b.removeChild(this._style);
154
+ (_d = (_c = this._meta) === null || _c === void 0 ? void 0 : _c.parentNode) === null || _d === void 0 ? void 0 : _d.removeChild(this._meta);
155
+ if (this.plugins.length > 0) {
156
+ this.plugins.forEach(plugin => {
157
+ var _a;
158
+ if (plugin.el) {
159
+ (plugin.el instanceof ShadowRoot ? plugin.el.host : plugin.el).remove();
160
+ }
161
+ (_a = plugin.onDestroy) === null || _a === void 0 ? void 0 : _a.call(plugin);
162
+ });
163
+ this.plugins = [];
164
+ }
165
+ }
98
166
  }
99
167
  Darkify.storageKey = 'theme';
100
168
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  *
3
3
  * @author Emilio Romero <emrocode@gmail.com>
4
- * @version 1.1.12
4
+ * @version 1.1.13-beta.0
5
5
  * @license MIT
6
6
  */
7
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Darkify=t()}(this,function(){"use strict";const e="undefined"!=typeof window,t={autoMatchTheme:!0,useLocalStorage:!0,useSessionStorage:!1,useColorScheme:["#ffffff","#000000"]};class s{constructor(s,o){if(this.options=t,this.theme="light",!e)return;const i=Object.assign(Object.assign({},t),o);i.useLocalStorage&&i.useSessionStorage&&(i.useSessionStorage=!1),this.options=i,this.theme=this.getOsPreference(this.options),this._style=document.createElement("style"),this._meta=document.createElement("meta"),this.init(s),this.createAttribute(),this.syncThemeBetweenTabs()}init(e){window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",({matches:e})=>{this.theme=e?"dark":"light",this.createAttribute()}),e&&document.addEventListener("DOMContentLoaded",()=>{const t=document.querySelector(e);null==t||t.addEventListener("click",()=>this.toggleTheme())})}getOsPreference(e){const{autoMatchTheme:t,useLocalStorage:o,useSessionStorage:i}=e;return o&&window.localStorage.getItem(s.storageKey)||i&&window.sessionStorage.getItem(s.storageKey)||(t&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light")}createAttribute(){const e=document.getElementsByTagName("html")[0],{useColorScheme:t}=this.options,s=`/**! Darkify / A simple dark mode toggle library **/\n:root:where([data-theme="${this.theme}"]),[data-theme="${this.theme}"]{color-scheme:${this.theme}}`;e.dataset.theme=this.theme,this.updateTags(s,t),this.savePreference()}updateTags(e,t){const[s,o]=t;this._meta.name="theme-color",this._meta.content="light"===this.theme?s:null!=o?o:s,this._style.innerHTML=e;const i=document.getElementsByTagName("head")[0];this._meta.parentNode||i.appendChild(this._meta),this._style.parentNode||i.appendChild(this._style)}savePreference(){const{useLocalStorage:e}=this.options,t=e?window.localStorage:window.sessionStorage;(e?window.sessionStorage:window.localStorage).removeItem(s.storageKey),t.setItem(s.storageKey,this.theme)}syncThemeBetweenTabs(){window.addEventListener("storage",e=>{e.key===s.storageKey&&e.newValue&&(this.theme=e.newValue,this.createAttribute())})}toggleTheme(){this.theme="light"===this.theme?"dark":"light",this.createAttribute()}getCurrentTheme(){return this.theme}}return s.storageKey="theme",s});
7
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Darkify=t()}(this,function(){"use strict";class e{constructor(){this.listeners=[]}addListener(e,t,s,i){e.addEventListener(t,s,i),this.listeners.push({target:e,event:t,handler:s,options:i})}clearListeners(){this.listeners.forEach(({target:e,event:t,handler:s,options:i})=>{e.removeEventListener(t,s,i)}),this.listeners=[]}}const t={autoMatchTheme:!0,useColorScheme:["#ffffff","#000000"],useStorage:"local"},s="undefined"!=typeof window;class i{constructor(i,n){if(this.options=t,this.plugins=[],this.theme="light",!s)return;this._elm=new e;const o=Object.assign(Object.assign({},t),n);this.options=o,this.theme=this.getOsPreference(),this._style=document.createElement("style"),this._meta=document.createElement("meta"),this.init(i),this.createAttribute(),this.syncThemeBetweenTabs()}init(e){this._elm.addListener(window.matchMedia("(prefers-color-scheme: dark)"),"change",({matches:e})=>{this.theme=e?"dark":"light",this.createAttribute()}),this.initPlugins();const t=this.plugins.some(e=>void 0!==e.el);e&&!t&&this._elm.addListener(document,"DOMContentLoaded",()=>{const t=document.querySelector(e);t&&this._elm.addListener(t,"click",()=>this.toggleTheme())})}initPlugins(){var e;null===(e=this.options.usePlugins)||void 0===e||e.forEach(e=>{const[t,s]=Array.isArray(e)?e:[e,void 0],i=new t(this,s),n=i.render();(n instanceof HTMLElement||n instanceof ShadowRoot)&&(i.el=n),this.plugins.push(i)})}notifyPlugins(e){this.plugins.forEach(t=>{var s;null===(s=t.onThemeChange)||void 0===s||s.call(t,e)})}getStorage(){const{useStorage:e}=this.options;if("none"!==e)return"local"===e?window.localStorage:window.sessionStorage}getOsPreference(){const e=this.getStorage();if(e){const t=e.getItem(i.storageKey);if(t)return t}return this.options.autoMatchTheme&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}createAttribute(){const e=document.documentElement,{useColorScheme:t}=this.options,s=`/**! Darkify / A simple dark mode toggle library **/\n:root:where([data-theme="${this.theme}"]),[data-theme="${this.theme}"]{color-scheme:${this.theme}}`;e.dataset.theme=this.theme,this.updateTags(s,t),this.savePreference()}updateTags(e,t){const[s,i]=t;this._meta.name="theme-color",this._meta.media=`(prefers-color-scheme: ${this.theme})`,this._meta.content="light"===this.theme?s:null!=i?i:s,this._style.innerHTML=e;const n=document.head;this._meta.parentNode||n.appendChild(this._meta),this._style.parentNode||n.appendChild(this._style)}savePreference(){const{useStorage:e}=this.options;if("none"===e)return;const t="local"===e,s=t?window.localStorage:window.sessionStorage;(t?window.sessionStorage:window.localStorage).removeItem(i.storageKey),s.setItem(i.storageKey,this.theme)}syncThemeBetweenTabs(){this._elm.addListener(window,"storage",e=>{e.key===i.storageKey&&e.newValue&&(this.theme=e.newValue,this.createAttribute(),this.notifyPlugins(e.newValue))})}setTheme(e){this.theme=e,this.createAttribute(),this.notifyPlugins(e)}toggleTheme(){this.setTheme("light"===this.theme?"dark":"light")}getCurrentTheme(){return this.theme}destroy(){var e,t,s,i;this._elm.clearListeners(),null===(t=null===(e=this._style)||void 0===e?void 0:e.parentNode)||void 0===t||t.removeChild(this._style),null===(i=null===(s=this._meta)||void 0===s?void 0:s.parentNode)||void 0===i||i.removeChild(this._meta),this.plugins.length>0&&(this.plugins.forEach(e=>{var t;e.el&&(e.el instanceof ShadowRoot?e.el.host:e.el).remove(),null===(t=e.onDestroy)||void 0===t||t.call(e)}),this.plugins=[])}}return i.storageKey="theme",i});
@@ -0,0 +1,54 @@
1
+ interface DarkifyPlugin {
2
+ el?: HTMLElement | ShadowRoot;
3
+ render(): void | HTMLElement | ShadowRoot;
4
+ onThemeChange?: (theme: string) => void;
5
+ onDestroy?: () => void;
6
+ }
7
+
8
+ interface ThemeWidgetOptions {
9
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
10
+ size?: 'small' | 'medium' | 'large';
11
+ shortcut?: string;
12
+ }
13
+ declare class ThemeWidget implements DarkifyPlugin {
14
+ el?: ShadowRoot;
15
+ private _host;
16
+ static readonly pluginId = "d-widget";
17
+ private options;
18
+ /**
19
+ * Creates a theme toggle widget button
20
+ * @param host - The darkify instance that controls theme state
21
+ * @param options - Widget configuration (position, size, shortcut)
22
+ */
23
+ constructor(host: any, options?: ThemeWidgetOptions);
24
+ render(): ShadowRoot;
25
+ onThemeChange(theme: string): void;
26
+ onDestroy(): void;
27
+ private getPositionVars;
28
+ private getSizeVar;
29
+ }
30
+
31
+ interface KeyboardShortcutOptions {
32
+ key?: string;
33
+ ctrl?: boolean;
34
+ shift?: boolean;
35
+ target?: 'body' | 'input' | 'all';
36
+ }
37
+ declare class KeyboardShortcut implements DarkifyPlugin {
38
+ private _host;
39
+ private options;
40
+ /**
41
+ * Creates a keyboard shortcut listener for theme toggling
42
+ * @param host - The darkify instance that controls theme state
43
+ * @param options - Shortcut configuration (key, ctrl, shift, target)
44
+ */
45
+ constructor(host: any, options?: KeyboardShortcutOptions);
46
+ private handleKeyDown;
47
+ render(): void;
48
+ onDestroy(): void;
49
+ private matches;
50
+ private isTyping;
51
+ }
52
+
53
+ export { KeyboardShortcut, ThemeWidget };
54
+ export type { KeyboardShortcutOptions, ThemeWidgetOptions };
@@ -0,0 +1,226 @@
1
+ class ThemeWidget {
2
+ constructor(host, options) {
3
+ var _a, _b, _c;
4
+ this._host = host;
5
+ this.options = {
6
+ position: (_a = options === null || options === void 0 ? void 0 : options.position) !== null && _a !== void 0 ? _a : 'bottom-right',
7
+ size: (_b = options === null || options === void 0 ? void 0 : options.size) !== null && _b !== void 0 ? _b : 'medium',
8
+ shortcut: (_c = options === null || options === void 0 ? void 0 : options.shortcut) !== null && _c !== void 0 ? _c : '',
9
+ };
10
+ }
11
+ render() {
12
+ var _a;
13
+ const container = document.createElement('div');
14
+ container.id = ThemeWidget.pluginId;
15
+ this.el = container.attachShadow({ mode: 'open' });
16
+ const positionVars = this.getPositionVars();
17
+ const sizeVars = this.getSizeVar();
18
+ const sheet = new CSSStyleSheet();
19
+ sheet.replaceSync(`
20
+ :host {
21
+ --widget-safe-top: env(safe-area-inset-top, 0px);
22
+ --widget-safe-right: env(safe-area-inset-right, 0px);
23
+ --widget-safe-bottom: env(safe-area-inset-bottom, 0px);
24
+ --widget-safe-left: env(safe-area-inset-left, 0px);
25
+ --widget-margin: 24px;
26
+ }
27
+
28
+ .d-wrapper {
29
+ --margin: max(var(--widget-margin), var(--widget-safe-bottom));
30
+ ${positionVars}
31
+ ${sizeVars}
32
+ position: fixed;
33
+ top: calc(var(--pos-top) + var(--widget-safe-top));
34
+ right: calc(var(--pos-right) + var(--widget-safe-right));
35
+ bottom: calc(var(--pos-bottom) + var(--widget-safe-bottom));
36
+ left: calc(var(--pos-left) + var(--widget-safe-left));
37
+ z-index: 9999;
38
+ max-width: fit-content;
39
+ display: flex;
40
+ align-items: center;
41
+ column-gap: 0.5rem;
42
+ }
43
+
44
+ .d-button {
45
+ --icon-size: calc(var(--size) * 0.4);
46
+ position: relative;
47
+ width: var(--size);
48
+ height: var(--size);
49
+ border-radius: 50%;
50
+ border: none;
51
+ border-bottom: 2px solid var(--widget-accent, hsl(0,0%,0%,0.30));
52
+ box-shadow: 0 1px 3px 0 hsla(210,6%,25%,0.30), 0 4px 8px 3px hsla(210,6%,25%,0.30);
53
+ background-color: transparent;
54
+ color: canvastext;
55
+ cursor: pointer;
56
+ font-size: var(--icon-size);
57
+ display: flex;
58
+ flex-direction: column;
59
+ align-items: center;
60
+ justify-content: center;
61
+ overflow: hidden;
62
+ transition: transform 0.4s ease-in-out;
63
+ user-select: none;
64
+ -webkit-user-select: none;
65
+ -webkit-tap-highlight-color: transparent;
66
+ }
67
+
68
+ .d-button::before {
69
+ content: "";
70
+ position: absolute;
71
+ inset: 0;
72
+ z-index: -1;
73
+ border: none;
74
+ border-radius: 50%;
75
+ background-color: canvas;
76
+ filter: invert(90%);
77
+ }
78
+
79
+ .d-button.light { --widget-accent: hsl(45,99%,54%); }
80
+ .d-button.dark { --widget-accent: hsl(200,29%,43%); }
81
+
82
+ @media (prefers-reduced-motion: no-preference) {
83
+ .d-button {
84
+ animation: dEnter 0.4s cubic-bezier(0.34,1.56,0.64,1);
85
+ animation-fill-mode: backwards;
86
+ }
87
+ }
88
+
89
+ @keyframes dEnter {
90
+ from {
91
+ opacity: 0;
92
+ transform: scale(0.6);
93
+ }
94
+ to {
95
+ opacity: 1;
96
+ transform: scale(1);
97
+ }
98
+ }
99
+
100
+ .d-kbd {
101
+ position: relative;
102
+ padding: 0.25em 0.4em;
103
+ font-size: 11px;
104
+ font-family: ui-monospace, monospace;
105
+ line-height: 1;
106
+ letter-spacing: -0.025em;
107
+ background-color: canvas;
108
+ color: canvastext;
109
+ filter: invert(90%);
110
+ border: none;
111
+ border-bottom: 2px solid hsl(from canvastext h s l / calc(alpha * 0.5));
112
+ border-radius: 0.25rem;
113
+ box-shadow: 0 0 2px hsla(0,0%,0%,0.10);
114
+ user-select: none;
115
+ -webkit-user-select: none;
116
+ }
117
+ `);
118
+ this.el.adoptedStyleSheets = [sheet];
119
+ const wrapper = document.createElement('div');
120
+ wrapper.className = 'd-wrapper';
121
+ const button = document.createElement('button');
122
+ button.className = 'd-button';
123
+ button.setAttribute('aria-label', 'Toggle theme');
124
+ button.addEventListener('click', () => this._host.toggleTheme());
125
+ const icon = document.createElement('span');
126
+ icon.className = 'd-icon';
127
+ button.appendChild(icon);
128
+ const isLeftPosition = (_a = this.options.position) === null || _a === void 0 ? void 0 : _a.includes('left');
129
+ if (this.options.shortcut) {
130
+ const kbd = document.createElement('kbd');
131
+ kbd.className = 'd-kbd';
132
+ kbd.textContent = this.options.shortcut;
133
+ if (isLeftPosition) {
134
+ wrapper.appendChild(button);
135
+ wrapper.appendChild(kbd);
136
+ }
137
+ else {
138
+ wrapper.appendChild(kbd);
139
+ wrapper.appendChild(button);
140
+ }
141
+ }
142
+ else {
143
+ wrapper.appendChild(button);
144
+ }
145
+ this.el.appendChild(wrapper);
146
+ document.body.appendChild(container);
147
+ this.onThemeChange(this._host.getCurrentTheme());
148
+ return this.el;
149
+ }
150
+ onThemeChange(theme) {
151
+ var _a, _b;
152
+ const button = (_a = this.el) === null || _a === void 0 ? void 0 : _a.querySelector('button.d-button');
153
+ const icon = (_b = this.el) === null || _b === void 0 ? void 0 : _b.querySelector('span.d-icon');
154
+ if (button && icon) {
155
+ button.className = `d-button ${theme}`;
156
+ icon.textContent = theme === 'light' ? '🌞' : '🌚';
157
+ }
158
+ }
159
+ onDestroy() {
160
+ if (this.el && this.el.host) {
161
+ this.el.host.remove();
162
+ }
163
+ }
164
+ getPositionVars() {
165
+ const vars = {
166
+ 'top-left': '--pos-top:var(--margin);--pos-right:auto;--pos-bottom:auto;--pos-left:var(--margin);',
167
+ 'top-right': '--pos-top:var(--margin);--pos-right:var(--margin);--pos-bottom:auto;--pos-left:auto;',
168
+ 'bottom-left': '--pos-top:auto;--pos-right:auto;--pos-bottom:calc(var(--margin)*2);--pos-left:var(--margin);',
169
+ 'bottom-right': '--pos-top:auto;--pos-right:var(--margin);--pos-bottom:calc(var(--margin)*2);--pos-left:auto;',
170
+ };
171
+ return vars[this.options.position];
172
+ }
173
+ getSizeVar() {
174
+ const vars = {
175
+ small: '--size:36px;',
176
+ medium: '--size:56px;',
177
+ large: '--size:72px;',
178
+ };
179
+ return vars[this.options.size];
180
+ }
181
+ }
182
+ ThemeWidget.pluginId = 'd-widget';
183
+
184
+ class KeyboardShortcut {
185
+ constructor(host, options) {
186
+ var _a, _b, _c, _d;
187
+ this.handleKeyDown = (e) => {
188
+ if (this.options.target === 'body' && this.isTyping(e))
189
+ return;
190
+ if (this.options.target === 'input' && !this.isTyping(e))
191
+ return;
192
+ if (this.matches(e)) {
193
+ e.preventDefault();
194
+ this._host.toggleTheme();
195
+ }
196
+ };
197
+ this._host = host;
198
+ this.options = {
199
+ key: (_a = options === null || options === void 0 ? void 0 : options.key) !== null && _a !== void 0 ? _a : 'd',
200
+ ctrl: (_b = options === null || options === void 0 ? void 0 : options.ctrl) !== null && _b !== void 0 ? _b : false,
201
+ shift: (_c = options === null || options === void 0 ? void 0 : options.shift) !== null && _c !== void 0 ? _c : false,
202
+ target: (_d = options === null || options === void 0 ? void 0 : options.target) !== null && _d !== void 0 ? _d : 'body',
203
+ };
204
+ }
205
+ render() {
206
+ document.addEventListener('keydown', this.handleKeyDown);
207
+ return;
208
+ }
209
+ onDestroy() {
210
+ document.removeEventListener('keydown', this.handleKeyDown);
211
+ }
212
+ matches(e) {
213
+ return (e.key.toLowerCase() === this.options.key.toLowerCase() &&
214
+ (!this.options.ctrl || e.ctrlKey || e.metaKey) &&
215
+ (!this.options.shift || e.shiftKey) &&
216
+ (this.options.ctrl || (!e.ctrlKey && !e.metaKey && !e.altKey)));
217
+ }
218
+ isTyping(e) {
219
+ const target = e.target;
220
+ const tagName = target.tagName.toLowerCase();
221
+ const isEditable = target.isContentEditable || tagName === 'input' || tagName === 'textarea';
222
+ return isEditable;
223
+ }
224
+ }
225
+
226
+ export { KeyboardShortcut, ThemeWidget };
@@ -0,0 +1 @@
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).DarkifyPlugins={})}(this,function(t){"use strict";class e{constructor(t,e){var n,o,i;this._host=t,this.options={position:null!==(n=null==e?void 0:e.position)&&void 0!==n?n:"bottom-right",size:null!==(o=null==e?void 0:e.size)&&void 0!==o?o:"medium",shortcut:null!==(i=null==e?void 0:e.shortcut)&&void 0!==i?i:""}}render(){var t;const n=document.createElement("div");n.id=e.pluginId,this.el=n.attachShadow({mode:"open"});const o=this.getPositionVars(),i=this.getSizeVar(),s=new CSSStyleSheet;s.replaceSync(`\n :host {\n --widget-safe-top: env(safe-area-inset-top, 0px);\n --widget-safe-right: env(safe-area-inset-right, 0px);\n --widget-safe-bottom: env(safe-area-inset-bottom, 0px);\n --widget-safe-left: env(safe-area-inset-left, 0px);\n --widget-margin: 24px;\n }\n\n .d-wrapper {\n --margin: max(var(--widget-margin), var(--widget-safe-bottom));\n ${o}\n ${i}\n position: fixed;\n top: calc(var(--pos-top) + var(--widget-safe-top));\n right: calc(var(--pos-right) + var(--widget-safe-right));\n bottom: calc(var(--pos-bottom) + var(--widget-safe-bottom));\n left: calc(var(--pos-left) + var(--widget-safe-left));\n z-index: 9999;\n max-width: fit-content;\n display: flex;\n align-items: center;\n column-gap: 0.5rem;\n }\n\n .d-button {\n --icon-size: calc(var(--size) * 0.4);\n position: relative;\n width: var(--size);\n height: var(--size);\n border-radius: 50%;\n border: none;\n border-bottom: 2px solid var(--widget-accent, hsl(0,0%,0%,0.30));\n box-shadow: 0 1px 3px 0 hsla(210,6%,25%,0.30), 0 4px 8px 3px hsla(210,6%,25%,0.30);\n background-color: transparent;\n color: canvastext;\n cursor: pointer;\n font-size: var(--icon-size);\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n transition: transform 0.4s ease-in-out;\n user-select: none;\n -webkit-user-select: none;\n -webkit-tap-highlight-color: transparent;\n }\n\n .d-button::before {\n content: "";\n position: absolute;\n inset: 0;\n z-index: -1;\n border: none;\n border-radius: 50%;\n background-color: canvas;\n filter: invert(90%);\n }\n\n .d-button.light { --widget-accent: hsl(45,99%,54%); }\n .d-button.dark { --widget-accent: hsl(200,29%,43%); }\n\n @media (prefers-reduced-motion: no-preference) {\n .d-button {\n animation: dEnter 0.4s cubic-bezier(0.34,1.56,0.64,1);\n animation-fill-mode: backwards;\n }\n }\n\n @keyframes dEnter {\n from {\n opacity: 0;\n transform: scale(0.6);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n }\n\n .d-kbd {\n position: relative;\n padding: 0.25em 0.4em;\n font-size: 11px;\n font-family: ui-monospace, monospace;\n line-height: 1;\n letter-spacing: -0.025em;\n background-color: canvas;\n color: canvastext;\n filter: invert(90%);\n border: none;\n border-bottom: 2px solid hsl(from canvastext h s l / calc(alpha * 0.5));\n border-radius: 0.25rem;\n box-shadow: 0 0 2px hsla(0,0%,0%,0.10);\n user-select: none;\n -webkit-user-select: none;\n }\n `),this.el.adoptedStyleSheets=[s];const a=document.createElement("div");a.className="d-wrapper";const r=document.createElement("button");r.className="d-button",r.setAttribute("aria-label","Toggle theme"),r.addEventListener("click",()=>this._host.toggleTheme());const l=document.createElement("span");l.className="d-icon",r.appendChild(l);const d=null===(t=this.options.position)||void 0===t?void 0:t.includes("left");if(this.options.shortcut){const t=document.createElement("kbd");t.className="d-kbd",t.textContent=this.options.shortcut,d?(a.appendChild(r),a.appendChild(t)):(a.appendChild(t),a.appendChild(r))}else a.appendChild(r);return this.el.appendChild(a),document.body.appendChild(n),this.onThemeChange(this._host.getCurrentTheme()),this.el}onThemeChange(t){var e,n;const o=null===(e=this.el)||void 0===e?void 0:e.querySelector("button.d-button"),i=null===(n=this.el)||void 0===n?void 0:n.querySelector("span.d-icon");o&&i&&(o.className=`d-button ${t}`,i.textContent="light"===t?"🌞":"🌚")}onDestroy(){this.el&&this.el.host&&this.el.host.remove()}getPositionVars(){return{"top-left":"--pos-top:var(--margin);--pos-right:auto;--pos-bottom:auto;--pos-left:var(--margin);","top-right":"--pos-top:var(--margin);--pos-right:var(--margin);--pos-bottom:auto;--pos-left:auto;","bottom-left":"--pos-top:auto;--pos-right:auto;--pos-bottom:calc(var(--margin)*2);--pos-left:var(--margin);","bottom-right":"--pos-top:auto;--pos-right:var(--margin);--pos-bottom:calc(var(--margin)*2);--pos-left:auto;"}[this.options.position]}getSizeVar(){return{small:"--size:36px;",medium:"--size:56px;",large:"--size:72px;"}[this.options.size]}}e.pluginId="d-widget";t.KeyboardShortcut=class{constructor(t,e){var n,o,i,s;this.handleKeyDown=t=>{"body"===this.options.target&&this.isTyping(t)||("input"!==this.options.target||this.isTyping(t))&&this.matches(t)&&(t.preventDefault(),this._host.toggleTheme())},this._host=t,this.options={key:null!==(n=null==e?void 0:e.key)&&void 0!==n?n:"d",ctrl:null!==(o=null==e?void 0:e.ctrl)&&void 0!==o&&o,shift:null!==(i=null==e?void 0:e.shift)&&void 0!==i&&i,target:null!==(s=null==e?void 0:e.target)&&void 0!==s?s:"body"}}render(){document.addEventListener("keydown",this.handleKeyDown)}onDestroy(){document.removeEventListener("keydown",this.handleKeyDown)}matches(t){return t.key.toLowerCase()===this.options.key.toLowerCase()&&(!this.options.ctrl||t.ctrlKey||t.metaKey)&&(!this.options.shift||t.shiftKey)&&(this.options.ctrl||!t.ctrlKey&&!t.metaKey&&!t.altKey)}isTyping(t){const e=t.target,n=e.tagName.toLowerCase();return e.isContentEditable||"input"===n||"textarea"===n}},t.ThemeWidget=e});
package/package.json CHANGED
@@ -1,11 +1,23 @@
1
1
  {
2
2
  "name": "darkify-js",
3
- "version": "1.1.12",
3
+ "version": "1.1.13-beta.0",
4
4
  "description": "A simple dark mode toggle library",
5
5
  "type": "module",
6
6
  "main": "dist/darkify.umd.js",
7
7
  "module": "dist/darkify.esm.js",
8
8
  "types": "dist/darkify.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/darkify.esm.js",
12
+ "require": "./dist/darkify.umd.js",
13
+ "types": "./dist/darkify.d.ts"
14
+ },
15
+ "./plugins": {
16
+ "import": "./dist/plugins/index.mjs",
17
+ "require": "./dist/plugins/index.umd.js",
18
+ "types": "./dist/plugins/index.d.ts"
19
+ }
20
+ },
9
21
  "files": [
10
22
  "dist"
11
23
  ],
@@ -14,7 +26,8 @@
14
26
  "_bundle": "rollup -c rollup.config.ts --configPlugin typescript",
15
27
  "build": "npm run _cls && npm run _bundle",
16
28
  "format": "prettier --write src/",
17
- "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest"
29
+ "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest",
30
+ "prepublishOnly": "npm run test && npm run build"
18
31
  },
19
32
  "repository": {
20
33
  "type": "git",
@@ -33,15 +46,15 @@
33
46
  "author": "Emilio Romero <emrocode@gmail.com>",
34
47
  "license": "MIT",
35
48
  "devDependencies": {
36
- "@rollup/plugin-terser": "^0.4.4",
49
+ "@rollup/plugin-terser": "^1.0.0",
37
50
  "@rollup/plugin-typescript": "^12.3.0",
38
51
  "@types/jest": "^30.0.0",
39
- "jest": "^30.2.0",
40
- "jest-environment-jsdom": "^30.2.0",
52
+ "jest": "^30.3.0",
53
+ "jest-environment-jsdom": "^30.3.0",
41
54
  "prettier": "^3.8.1",
42
- "rollup": "^4.57.1",
55
+ "rollup": "^4.59.0",
43
56
  "rollup-plugin-cleanup": "^3.2.1",
44
- "rollup-plugin-dts": "^6.3.0",
57
+ "rollup-plugin-dts": "^6.4.0",
45
58
  "ts-jest": "^29.4.6",
46
59
  "ts-node": "^10.9.2",
47
60
  "tslib": "^2.8.1",