kigumi 0.8.2 → 0.9.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.
package/README.md CHANGED
@@ -59,21 +59,20 @@ npx kigumi init
59
59
 
60
60
  **Own Web Awesome Pro license?**
61
61
 
62
- To unlock premium themes and Pro-only components, you need a Web Awesome Pro account. [Get your token](https://webawesome.com/login) and choose one of the following token setup methods:
62
+ To unlock premium themes and Pro-only components, you need a Web Awesome Pro account. [Get your token](https://webawesome.com/login) and configure it:
63
63
 
64
64
  ```bash
65
-
66
65
  # Option 1: During `init` command (recommended)
67
66
  npx kigumi init --token YOUR_TOKEN
68
67
 
69
- # Option 2: Global
68
+ # Option 2: Set it globally (once per machine)
70
69
  npm config set //npm.cloudsmith.io/fortawesome/webawesome-pro/:_authToken YOUR_TOKEN
71
70
 
72
- # Option 3: In .env file
73
- WEBAWESOME_NPM_TOKEN=your_token
71
+ # Option 3: In project .env file
72
+ echo "WEBAWESOME_NPM_TOKEN=your_token" > .env
74
73
 
75
- # Option 4: In .npmrc file
76
- //npm.cloudsmith.io/fortawesome/webawesome-pro/:_authToken YOUR_TOKEN
74
+ # Option 4: Environment variable (CI/CD)
75
+ export WEBAWESOME_NPM_TOKEN=your_token
77
76
  ```
78
77
 
79
78
  ### `add`
@@ -200,7 +199,9 @@ If `npm install` fails with 401 errors:
200
199
  npm config get //npm.cloudsmith.io/fortawesome/webawesome-pro/:_authToken
201
200
  ```
202
201
 
203
- 3. Check your token is valid at [https://webawesome.com/login](https://https://webawesome.com/login)
202
+ 3. Check your token is valid at [https://webawesome.com/login](https://webawesome.com/login)
203
+
204
+ **Using pnpm?** pnpm does not use global auth for scoped registries. If 401 persists, add the token to your project `.npmrc` or use `npx kigumi init --token YOUR_TOKEN`.
204
205
 
205
206
  ### Component styles not loading
206
207
 
package/dist/index.js CHANGED
@@ -517,7 +517,7 @@ var ConfigNotFoundError = class extends KigumiError {
517
517
  // src/errors/validation.ts
518
518
  var ValidationError = class extends KigumiError {
519
519
  constructor(field, value, validValues, zodError) {
520
- const errorMessages = zodError?.errors.map((err) => {
520
+ const errorMessages = zodError?.issues.map((err) => {
521
521
  return `${err.path.join(".")}: ${err.message}`;
522
522
  }) || [];
523
523
  const suggestions = [
@@ -779,10 +779,10 @@ function handleError(error, output) {
779
779
  import { z } from "zod";
780
780
  var FRAMEWORKS = ["react", "vue", "svelte", "angular"];
781
781
  var frameworkSchema = z.enum(FRAMEWORKS, {
782
- errorMap: () => ({ message: `Must be one of: ${FRAMEWORKS.join(", ")}` })
782
+ error: () => `Must be one of: ${FRAMEWORKS.join(", ")}`
783
783
  });
784
784
  var tierSchema = z.enum(["free", "pro"], {
785
- errorMap: () => ({ message: 'Must be either "free" or "pro"' })
785
+ error: () => 'Must be either "free" or "pro"'
786
786
  });
787
787
  var themeConfigSchema = z.object({
788
788
  selected: z.string().min(1, "Theme name cannot be empty"),
@@ -796,7 +796,7 @@ var webAwesomeConfigSchema = z.object({
796
796
  var kigumiConfigSchema = z.object({
797
797
  framework: frameworkSchema,
798
798
  typescript: z.boolean({
799
- errorMap: () => ({ message: "Must be a boolean (true or false)" })
799
+ error: () => "Must be a boolean (true or false)"
800
800
  }),
801
801
  componentsDir: z.string().min(1, "Components directory cannot be empty"),
802
802
  utilsDir: z.string().min(1, "Utils directory cannot be empty").optional(),
@@ -875,7 +875,7 @@ var listOptionsSchema = z2.object({
875
875
  function validateOptions(schema, options) {
876
876
  const result = schema.safeParse(options);
877
877
  if (!result.success) {
878
- const errors = result.error.errors.map((err) => {
878
+ const errors = result.error.issues.map((err) => {
879
879
  const path15 = err.path.join(".");
880
880
  return path15 ? `${path15}: ${err.message}` : err.message;
881
881
  });
@@ -917,14 +917,10 @@ var AVAILABLE_BRAND_COLORS = [
917
917
  "gray"
918
918
  ];
919
919
  var paletteSchema = z3.enum(AVAILABLE_PALETTES, {
920
- errorMap: () => ({
921
- message: `Palette must be one of: ${AVAILABLE_PALETTES.join(", ")}`
922
- })
920
+ error: () => `Palette must be one of: ${AVAILABLE_PALETTES.join(", ")}`
923
921
  });
924
922
  var brandColorSchema = z3.enum(AVAILABLE_BRAND_COLORS, {
925
- errorMap: () => ({
926
- message: `Brand color must be one of: ${AVAILABLE_BRAND_COLORS.join(", ")}`
927
- })
923
+ error: () => `Brand color must be one of: ${AVAILABLE_BRAND_COLORS.join(", ")}`
928
924
  });
929
925
  var componentNameSchema = z3.string().min(1, "Component name cannot be empty");
930
926
 
@@ -1905,7 +1901,7 @@ ${ENV_TOKEN_KEY}=${token}
1905
1901
  return;
1906
1902
  }
1907
1903
  const envContent = `# Web Awesome Pro authentication token
1908
- # Get your token from https://webawesome.com/pro
1904
+ # Get your token from https://webawesome.com
1909
1905
  ${ENV_TOKEN_KEY}=${token}
1910
1906
  `;
1911
1907
  await fs7.writeFile(envPath, envContent);
@@ -1,28 +1,40 @@
1
1
  import React from 'react';
2
+ import { createPortal } from 'react-dom';
2
3
  import clsx from 'clsx';
3
4
  import '{{{importPath}}}';
4
5
  import './Dialog.css';
5
6
 
6
7
  /**
7
- * Dialogs display important prompts and information
8
+ * // Using data-dialog attribute (recommended)
9
+ * import { Dialog } from './components/ui';
10
+ * import { Button } from './components/ui';
8
11
  *
9
- * @example
10
- * // Using ref methods
11
- * const dialogRef = React.useRef(null);
12
- * <Dialog ref={dialogRef} label="Dialog Title">
13
- * <p>Dialog content</p>
14
- * <div slot="footer">
15
- * <Button data-dialog="close">Close</Button>
16
- * </div>
12
+ * <Button data-dialog="open dialog">Open FAQ</Button>
13
+ * <Dialog id="dialog" label="FAQ">
14
+ * <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
15
+ * <Button slot="footer" variant="brand" data-dialog="close">
16
+ * Close
17
+ * </Button>
17
18
  * </Dialog>
18
- * dialogRef.current?.show();
19
+ * Dialogs display important prompts and information
19
20
  *
21
+ * @example
20
22
  * // Using open prop (recommended)
21
23
  * const [open, setOpen] = React.useState(false);
22
24
  * <Dialog open={open} label="Dialog Title" onHide={() => setOpen(false)}>
23
25
  * <p>Dialog content</p>
26
+ * <Button slot="footer" variant="brand" onClick={() => setOpen(false)}>
27
+ * Close
28
+ * </Button>
24
29
  * </Dialog>
25
30
  *
31
+ * // Using ref methods (alternative)
32
+ * const dialogRef = React.useRef(null);
33
+ * <Dialog ref={dialogRef} label="Dialog Title">
34
+ * <p>Dialog content</p>
35
+ * </Dialog>
36
+ * dialogRef.current?.show();
37
+ *
26
38
  * @param {Object} props
27
39
  * @param {boolean} [props.open] - Indicates whether or not the dialog is open
28
40
  * @param {string} props.label - The dialog's label as displayed in the header
@@ -32,33 +44,21 @@ import './Dialog.css';
32
44
  * @param {function} [props.onAfterShow] - Event fired after the dialog is shown
33
45
  * @param {function} [props.onHide] - Event fired when the dialog is about to hide
34
46
  * @param {function} [props.onAfterHide] - Event fired after the dialog is hidden
47
+ * @param {string | string[]} [props['data-dialog']] - Data attribute for opening and closing declaratively
35
48
  * @param {string} [props.className] - Additional CSS classes
36
49
  * @param {React.ReactNode} [props.children] - Dialog content
37
50
  * @param {React.Ref} ref - Ref with methods: show(), hide(), requestClose()
38
51
  */
39
52
  export const Dialog = React.forwardRef(
40
- ({ children, className, onShow, onAfterShow, onHide, onAfterHide, ...props }, ref) => {
53
+ ({ children, className, open, onShow, onAfterShow, onHide, onAfterHide, ...props }, ref) => {
41
54
  const dialogRef = React.useRef(null);
42
55
 
43
56
  React.useImperativeHandle(
44
57
  ref,
45
58
  () => ({
46
- show: () => {
47
- if (dialogRef.current && typeof dialogRef.current.show === 'function') {
48
- dialogRef.current.show();
49
- }
50
- },
51
- hide: () => {
52
- // hide is an alias for requestClose for consistency with other components
53
- if (dialogRef.current && typeof dialogRef.current.requestClose === 'function') {
54
- dialogRef.current.requestClose();
55
- }
56
- },
57
- requestClose: () => {
58
- if (dialogRef.current && typeof dialogRef.current.requestClose === 'function') {
59
- dialogRef.current.requestClose();
60
- }
61
- },
59
+ show: () => dialogRef.current?.show?.(),
60
+ hide: () => dialogRef.current?.requestClose?.(),
61
+ requestClose: () => dialogRef.current?.requestClose?.(),
62
62
  get element() {
63
63
  return dialogRef.current;
64
64
  },
@@ -66,26 +66,28 @@ export const Dialog = React.forwardRef(
66
66
  []
67
67
  );
68
68
 
69
- // Setup event listeners
69
+ // Sync open prop with dialog element
70
70
  React.useEffect(() => {
71
71
  const el = dialogRef.current;
72
- if (!el) return;
73
-
74
- const handleShow = (e) => {
75
- if (onShow) onShow(e);
76
- };
72
+ if (!el || open === undefined) return;
77
73
 
78
- const handleAfterShow = (e) => {
79
- if (onAfterShow) onAfterShow(e);
80
- };
74
+ const isOpen = el.open ?? false;
75
+ if (open && !isOpen) {
76
+ el.show?.();
77
+ } else if (!open && isOpen) {
78
+ el.requestClose?.();
79
+ }
80
+ }, [open]);
81
81
 
82
- const handleHide = (e) => {
83
- if (onHide) onHide(e);
84
- };
82
+ // Setup event listeners
83
+ React.useEffect(() => {
84
+ const el = dialogRef.current;
85
+ if (!el) return;
85
86
 
86
- const handleAfterHide = (e) => {
87
- if (onAfterHide) onAfterHide(e);
88
- };
87
+ const handleShow = (e) => onShow?.(e);
88
+ const handleAfterShow = (e) => onAfterShow?.(e);
89
+ const handleHide = (e) => onHide?.(e);
90
+ const handleAfterHide = (e) => onAfterHide?.(e);
89
91
 
90
92
  el.addEventListener('wa-show', handleShow);
91
93
  el.addEventListener('wa-after-show', handleAfterShow);
@@ -100,14 +102,15 @@ export const Dialog = React.forwardRef(
100
102
  };
101
103
  }, [onShow, onAfterShow, onHide, onAfterHide]);
102
104
 
103
- return (
105
+ return createPortal(
104
106
  <wa-dialog
105
107
  ref={dialogRef}
106
108
  class={clsx('Dialog', className)}
107
109
  {...props}
108
110
  >
109
111
  {children}
110
- </wa-dialog>
112
+ </wa-dialog>,
113
+ document.body
111
114
  );
112
115
  }
113
116
  );
@@ -1,20 +1,55 @@
1
1
  import { forwardRef, useRef, useImperativeHandle, useEffect, type HTMLAttributes } from 'react';
2
+ import { createPortal } from 'react-dom';
2
3
  import clsx from 'clsx';
3
4
  import '{{{importPath}}}';
4
5
  import './Dialog.css';
5
6
 
6
7
  /**
8
+ * // Using data-dialog attribute (recommended)
9
+ * import { Dialog } from './components/ui';
10
+ * import { Button } from './components/ui';
11
+ *
12
+ * <Button data-dialog="open dialog">Open FAQ</Button>
13
+ * <Dialog id="dialog" label="FAQ">
14
+ * <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
15
+ * <Button slot="footer" variant="brand" data-dialog="close">
16
+ * Close
17
+ * </Button>
18
+ * </Dialog>
7
19
  * Dialogs display important prompts and information
8
20
  *
9
21
  * @example
10
22
  * ```tsx
11
- * // Basic usage
12
- * <Dialog />
23
+ * // Using open prop (recommended)
24
+ * import { useState } from 'react';
25
+ *
26
+ * function App() {
27
+ * const [open, setOpen] = useState(false);
13
28
  *
14
- * // With event handlers
15
- * <Dialog
16
- * onShow={(e) => console.log(e)} />
29
+ * return (
30
+ * <>
31
+ * <Button onClick={() => setOpen(true)}>Open Dialog</Button>
32
+ * <Dialog open={open} label="Dialog Title" onHide={() => setOpen(false)}>
33
+ * <p>Dialog content</p>
34
+ * <Button slot="footer" variant="brand" onClick={() => setOpen(false)}>
35
+ * Close
36
+ * </Button>
37
+ * </Dialog>
38
+ * </>
39
+ * );
40
+ * }
17
41
  *
42
+ * // Using ref methods (alternative)
43
+ * import { useRef } from 'react';
44
+ * const dialogRef = useRef<DialogRef>(null);
45
+ *
46
+ * <Button onClick={() => dialogRef.current?.show()}>Open Dialog</Button>
47
+ * <Dialog ref={dialogRef} label="Dialog Title">
48
+ * <p>Dialog content</p>
49
+ * <Button slot="footer" variant="brand" onClick={() => dialogRef.current?.hide()}>
50
+ * Close
51
+ * </Button>
52
+ * </Dialog>
18
53
  * ```
19
54
  */
20
55
  export interface DialogProps extends Omit<HTMLAttributes<HTMLElement>, 'onShow' | 'onAfterShow' | 'onHide' | 'onAfterHide' | 'dir'> {
@@ -42,21 +77,33 @@ export interface DialogProps extends Omit<HTMLAttributes<HTMLElement>, 'onShow'
42
77
 
43
78
  /** Emitted after the dialog closes and all animations are complete. */
44
79
  onAfterHide?: (event: CustomEvent) => void;
80
+
81
+ /** Data attribute for opening and closing declaratively. */
82
+ 'data-dialog'?: string | string[];
45
83
  }
46
84
 
47
85
  export interface DialogRef {
86
+ show: () => void;
87
+ hide: () => void;
88
+ requestClose: () => void;
48
89
  /** Reference to the underlying HTML element */
49
90
  element: HTMLElement | null;
50
91
  }
51
92
 
52
93
  export const Dialog = forwardRef<DialogRef, DialogProps>(
53
- ({ children, className, onShow, onAfterShow, onHide, onAfterHide, ...props }, ref) => {
94
+ ({ children, className, open, onShow, onAfterShow, onHide, onAfterHide, ...props }, ref) => {
54
95
  const dialogRef = useRef<HTMLElement & {
96
+ show?: () => void;
97
+ requestClose?: () => void;
98
+ open?: boolean;
55
99
  }>(null);
56
100
 
57
101
  useImperativeHandle(
58
102
  ref,
59
103
  () => ({
104
+ show: () => dialogRef.current?.show?.(),
105
+ hide: () => dialogRef.current?.requestClose?.(),
106
+ requestClose: () => dialogRef.current?.requestClose?.(),
60
107
  get element() {
61
108
  return dialogRef.current;
62
109
  },
@@ -64,25 +111,28 @@ export const Dialog = forwardRef<DialogRef, DialogProps>(
64
111
  []
65
112
  );
66
113
 
114
+ // Sync open prop with dialog element
67
115
  useEffect(() => {
68
116
  const el = dialogRef.current;
69
- if (!el) return;
117
+ if (!el || open === undefined) return;
70
118
 
71
- const handleShow = (e: Event) => {
72
- if (onShow) onShow(e as CustomEvent);
73
- };
119
+ const isOpen = el.open ?? false;
120
+ if (open && !isOpen) {
121
+ el.show?.();
122
+ } else if (!open && isOpen) {
123
+ el.requestClose?.();
124
+ }
125
+ }, [open]);
74
126
 
75
- const handleAfterShow = (e: Event) => {
76
- if (onAfterShow) onAfterShow(e as CustomEvent);
77
- };
78
-
79
- const handleHide = (e: Event) => {
80
- if (onHide) onHide(e as CustomEvent);
81
- };
127
+ // Setup event listeners
128
+ useEffect(() => {
129
+ const el = dialogRef.current;
130
+ if (!el) return;
82
131
 
83
- const handleAfterHide = (e: Event) => {
84
- if (onAfterHide) onAfterHide(e as CustomEvent);
85
- };
132
+ const handleShow = (e: Event) => onShow?.(e as CustomEvent);
133
+ const handleAfterShow = (e: Event) => onAfterShow?.(e as CustomEvent);
134
+ const handleHide = (e: Event) => onHide?.(e as CustomEvent);
135
+ const handleAfterHide = (e: Event) => onAfterHide?.(e as CustomEvent);
86
136
 
87
137
  el.addEventListener('wa-show', handleShow);
88
138
  el.addEventListener('wa-after-show', handleAfterShow);
@@ -97,14 +147,15 @@ export const Dialog = forwardRef<DialogRef, DialogProps>(
97
147
  };
98
148
  }, [onShow, onAfterShow, onHide, onAfterHide]);
99
149
 
100
- return (
150
+ return createPortal(
101
151
  <wa-dialog
102
152
  ref={dialogRef}
103
153
  class={clsx('Dialog', className)}
104
154
  {...(props as Record<string, unknown>)}
105
155
  >
106
156
  {children}
107
- </wa-dialog>
157
+ </wa-dialog>,
158
+ document.body
108
159
  );
109
160
  }
110
161
  );
@@ -1,15 +1,55 @@
1
1
  import React from 'react';
2
+ import { createPortal } from 'react-dom';
2
3
  import clsx from 'clsx';
3
4
  import '{{{importPath}}}';
4
5
  import './Drawer.css';
5
6
 
6
7
  /**
8
+ * // Using data-drawer attribute (recommended)
9
+ * import { Drawer } from './components/ui';
10
+ * import { Button } from './components/ui';
11
+ *
12
+ * <Button data-drawer="open drawer">Open Drawer</Button>
13
+ * <Drawer id="drawer" label="Drawer Title">
14
+ * <p>Drawer content</p>
15
+ * <Button slot="footer" variant="brand" data-drawer="close">Close</Button>
16
+ * </Drawer>
17
+ * Drawers slide in from a container edge to expose additional options
18
+ *
19
+ * @example
20
+ * // Using open prop (recommended)
21
+ * const [open, setOpen] = React.useState(false);
22
+ * <Drawer open={open} label="Drawer Title" onHide={() => setOpen(false)}>
23
+ * <p>Drawer content</p>
24
+ * </Drawer>
25
+ *
26
+ * // Using ref methods (alternative)
27
+ * const drawerRef = React.useRef(null);
28
+ * <Drawer ref={drawerRef} label="Drawer Title">
29
+ * <p>Drawer content</p>
30
+ * </Drawer>
31
+ * drawerRef.current?.show();
32
+ *
7
33
  * @typedef {Object} DrawerRef
8
34
  * @property {() => void} show
9
35
  * @property {() => void} hide
10
36
  * @property {HTMLElement | null} element
37
+ *
38
+ * @param {Object} props
39
+ * @param {boolean} [props.open] - Indicates whether the drawer is open
40
+ * @param {string} [props.label] - The drawer's label as displayed in the header
41
+ * @param {'top' | 'end' | 'bottom' | 'start'} [props.placement] - The direction from which the drawer will open
42
+ * @param {boolean} [props['light-dismiss']] - Closes the drawer when the user clicks outside of it
43
+ * @param {boolean} [props['without-header']] - Removes the header
44
+ * @param {function} [props.onShow] - Event fired when the drawer is shown
45
+ * @param {function} [props.onAfterShow] - Event fired after the drawer is shown
46
+ * @param {function} [props.onHide] - Event fired when the drawer is about to hide
47
+ * @param {function} [props.onAfterHide] - Event fired after the drawer is hidden
48
+ * @param {string | string[]} [props['data-drawer']] - Data attribute for opening and closing declaratively
49
+ * @param {string} [props.className] - Additional CSS classes
50
+ * @param {React.ReactNode} [props.children] - Drawer content
51
+ * @param {React.Ref} ref - Ref with methods: show(), hide()
11
52
  */
12
-
13
53
  export const Drawer = React.forwardRef(
14
54
  ({ children, className, open, onShow, onAfterShow, onHide, onAfterHide, ...props }, ref) => {
15
55
  const drawerRef = React.useRef(null);
@@ -26,6 +66,7 @@ export const Drawer = React.forwardRef(
26
66
  []
27
67
  );
28
68
 
69
+ // Sync open prop with drawer element
29
70
  React.useEffect(() => {
30
71
  const el = drawerRef.current;
31
72
  if (!el || open === undefined) return;
@@ -38,6 +79,7 @@ export const Drawer = React.forwardRef(
38
79
  }
39
80
  }, [open]);
40
81
 
82
+ // Setup event listeners
41
83
  React.useEffect(() => {
42
84
  const el = drawerRef.current;
43
85
  if (!el) return;
@@ -60,14 +102,15 @@ export const Drawer = React.forwardRef(
60
102
  };
61
103
  }, [onShow, onAfterShow, onHide, onAfterHide]);
62
104
 
63
- return (
105
+ return createPortal(
64
106
  <wa-drawer
65
107
  ref={drawerRef}
66
108
  class={clsx('Drawer', className)}
67
109
  {...props}
68
110
  >
69
111
  {children}
70
- </wa-drawer>
112
+ </wa-drawer>,
113
+ document.body
71
114
  );
72
115
  }
73
116
  );
@@ -1,20 +1,47 @@
1
1
  import { forwardRef, useRef, useImperativeHandle, useEffect, type HTMLAttributes } from 'react';
2
+ import { createPortal } from 'react-dom';
2
3
  import clsx from 'clsx';
3
4
  import '{{{importPath}}}';
4
5
  import './Drawer.css';
5
6
 
6
7
  /**
8
+ * // Using data-drawer attribute (recommended)
9
+ * import { Drawer } from './components/ui';
10
+ * import { Button } from './components/ui';
11
+ *
12
+ * <Button data-drawer="open drawer">Open Drawer</Button>
13
+ * <Drawer id="drawer" label="Drawer Title">
14
+ * <p>Drawer content</p>
15
+ * <Button slot="footer" variant="brand" data-drawer="close">Close</Button>
16
+ * </Drawer>
7
17
  * Drawers slide in from a container edge to expose additional options
8
18
  *
9
19
  * @example
10
20
  * ```tsx
11
- * // Basic usage
12
- * <Drawer />
21
+ * // Using open prop (recommended)
22
+ * import { useState } from 'react';
23
+ *
24
+ * function App() {
25
+ * const [open, setOpen] = useState(false);
13
26
  *
14
- * // With event handlers
15
- * <Drawer
16
- * onShow={(e) => console.log(e)} />
27
+ * return (
28
+ * <>
29
+ * <Button onClick={() => setOpen(true)}>Open Drawer</Button>
30
+ * <Drawer open={open} label="Drawer Title" onHide={() => setOpen(false)}>
31
+ * <p>Drawer content</p>
32
+ * </Drawer>
33
+ * </>
34
+ * );
35
+ * }
17
36
  *
37
+ * // Using ref methods (alternative)
38
+ * import { useRef } from 'react';
39
+ * const drawerRef = useRef<DrawerRef>(null);
40
+ *
41
+ * <Button onClick={() => drawerRef.current?.show()}>Open Drawer</Button>
42
+ * <Drawer ref={drawerRef} label="Drawer Title">
43
+ * <p>Drawer content</p>
44
+ * </Drawer>
18
45
  * ```
19
46
  */
20
47
  export interface DrawerProps extends Omit<HTMLAttributes<HTMLElement>, 'onShow' | 'onAfterShow' | 'onHide' | 'onAfterHide' | 'dir'> {
@@ -45,21 +72,31 @@ export interface DrawerProps extends Omit<HTMLAttributes<HTMLElement>, 'onShow'
45
72
 
46
73
  /** Emitted after the drawer closes and all animations are complete. */
47
74
  onAfterHide?: (event: CustomEvent) => void;
75
+
76
+ /** Data attribute for opening and closing declaratively. */
77
+ 'data-drawer'?: string | string[];
48
78
  }
49
79
 
50
80
  export interface DrawerRef {
81
+ show: () => void;
82
+ hide: () => void;
51
83
  /** Reference to the underlying HTML element */
52
84
  element: HTMLElement | null;
53
85
  }
54
86
 
55
87
  export const Drawer = forwardRef<DrawerRef, DrawerProps>(
56
- ({ children, className, onShow, onAfterShow, onHide, onAfterHide, ...props }, ref) => {
88
+ ({ children, className, open, onShow, onAfterShow, onHide, onAfterHide, ...props }, ref) => {
57
89
  const drawerRef = useRef<HTMLElement & {
90
+ show?: () => void;
91
+ hide?: () => void;
92
+ open?: boolean;
58
93
  }>(null);
59
94
 
60
95
  useImperativeHandle(
61
96
  ref,
62
97
  () => ({
98
+ show: () => drawerRef.current?.show?.(),
99
+ hide: () => drawerRef.current?.hide?.(),
63
100
  get element() {
64
101
  return drawerRef.current;
65
102
  },
@@ -67,25 +104,28 @@ export const Drawer = forwardRef<DrawerRef, DrawerProps>(
67
104
  []
68
105
  );
69
106
 
107
+ // Sync open prop with drawer element
70
108
  useEffect(() => {
71
109
  const el = drawerRef.current;
72
- if (!el) return;
110
+ if (!el || open === undefined) return;
73
111
 
74
- const handleShow = (e: Event) => {
75
- if (onShow) onShow(e as CustomEvent);
76
- };
112
+ const isOpen = el.open ?? false;
113
+ if (open && !isOpen) {
114
+ el.show?.();
115
+ } else if (!open && isOpen) {
116
+ el.hide?.();
117
+ }
118
+ }, [open]);
77
119
 
78
- const handleAfterShow = (e: Event) => {
79
- if (onAfterShow) onAfterShow(e as CustomEvent);
80
- };
81
-
82
- const handleHide = (e: Event) => {
83
- if (onHide) onHide(e as CustomEvent);
84
- };
120
+ // Setup event listeners
121
+ useEffect(() => {
122
+ const el = drawerRef.current;
123
+ if (!el) return;
85
124
 
86
- const handleAfterHide = (e: Event) => {
87
- if (onAfterHide) onAfterHide(e as CustomEvent);
88
- };
125
+ const handleShow = (e: Event) => onShow?.(e as CustomEvent);
126
+ const handleAfterShow = (e: Event) => onAfterShow?.(e as CustomEvent);
127
+ const handleHide = (e: Event) => onHide?.(e as CustomEvent);
128
+ const handleAfterHide = (e: Event) => onAfterHide?.(e as CustomEvent);
89
129
 
90
130
  el.addEventListener('wa-show', handleShow);
91
131
  el.addEventListener('wa-after-show', handleAfterShow);
@@ -100,14 +140,15 @@ export const Drawer = forwardRef<DrawerRef, DrawerProps>(
100
140
  };
101
141
  }, [onShow, onAfterShow, onHide, onAfterHide]);
102
142
 
103
- return (
143
+ return createPortal(
104
144
  <wa-drawer
105
145
  ref={drawerRef}
106
146
  class={clsx('Drawer', className)}
107
147
  {...(props as Record<string, unknown>)}
108
148
  >
109
149
  {children}
110
- </wa-drawer>
150
+ </wa-drawer>,
151
+ document.body
111
152
  );
112
153
  }
113
154
  );