kigumi 0.9.0 → 0.9.2

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 (32) hide show
  1. package/dist/index.js +7 -11
  2. package/dist/skills/kigumi-react/references/event-mapping.md +36 -17
  3. package/dist/templates/AGENTS.md +19 -8
  4. package/dist/templates/react/Button/Button.jsx.hbs +4 -4
  5. package/dist/templates/react/Dialog/Dialog.jsx.hbs +47 -44
  6. package/dist/templates/react/Dialog/Dialog.tsx.hbs +73 -22
  7. package/dist/templates/react/Drawer/Drawer.jsx.hbs +46 -3
  8. package/dist/templates/react/Drawer/Drawer.tsx.hbs +63 -22
  9. package/dist/templates/react/FileInput/FileInput.jsx.hbs +16 -9
  10. package/dist/templates/react/Input/Input.jsx.hbs +10 -10
  11. package/dist/templates/react/Input/Input.tsx.hbs +6 -6
  12. package/dist/templates/react/NumberInput/NumberInput.jsx.hbs +10 -10
  13. package/dist/templates/vue/Dialog/Dialog.js.vue.hbs +45 -12
  14. package/dist/templates/vue/Dialog/Dialog.vue.hbs +50 -12
  15. package/dist/templates/vue/Drawer/Drawer.js.vue.hbs +44 -13
  16. package/dist/templates/vue/Drawer/Drawer.vue.hbs +47 -12
  17. package/package.json +3 -2
  18. package/skills/kigumi-react/references/event-mapping.md +36 -17
  19. package/templates/AGENTS.md +19 -8
  20. package/templates/react/Button/Button.jsx.hbs +4 -4
  21. package/templates/react/Dialog/Dialog.jsx.hbs +47 -44
  22. package/templates/react/Dialog/Dialog.tsx.hbs +73 -22
  23. package/templates/react/Drawer/Drawer.jsx.hbs +46 -3
  24. package/templates/react/Drawer/Drawer.tsx.hbs +63 -22
  25. package/templates/react/FileInput/FileInput.jsx.hbs +16 -9
  26. package/templates/react/Input/Input.jsx.hbs +10 -10
  27. package/templates/react/Input/Input.tsx.hbs +6 -6
  28. package/templates/react/NumberInput/NumberInput.jsx.hbs +10 -10
  29. package/templates/vue/Dialog/Dialog.js.vue.hbs +45 -12
  30. package/templates/vue/Dialog/Dialog.vue.hbs +50 -12
  31. package/templates/vue/Drawer/Drawer.js.vue.hbs +44 -13
  32. package/templates/vue/Drawer/Drawer.vue.hbs +47 -12
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
 
@@ -1,26 +1,45 @@
1
1
  # Event Mapping
2
2
 
3
- Web Awesome components emit custom events prefixed with `wa-`. These are mapped to React event handlers.
3
+ Web Awesome components use two different event systems depending on the component type.
4
4
 
5
- ## Standard Events
5
+ ## Form Controls — Native DOM Events
6
6
 
7
- | Web Awesome Event | React Handler | Type |
8
- | ----------------- | --------------------- | ------------------------------ |
9
- | `wa-change` | `onChange` | `(event: CustomEvent) => void` |
10
- | `wa-input` | `onInput` | `(event: CustomEvent) => void` |
11
- | `wa-show` | `onShow` | `(event: CustomEvent) => void` |
12
- | `wa-hide` | `onHide` | `(event: CustomEvent) => void` |
13
- | `wa-after-show` | `onAfterShow` | `(event: CustomEvent) => void` |
14
- | `wa-after-hide` | `onAfterHide` | `(event: CustomEvent) => void` |
15
- | `wa-blur` | `onBlur` | `(event: FocusEvent) => void` |
16
- | `wa-focus` | `onFocus` | `(event: FocusEvent) => void` |
17
- | Standard events | Standard React events | Native types |
7
+ `wa-button`, `wa-input`, `wa-number-input`, `wa-file-input`, `wa-textarea`, `wa-select`, `wa-checkbox`, `wa-switch` emit **native browser events** (no `wa-` prefix). Event handlers receive standard `Event` / `FocusEvent` objects — use `e.target.value`, `e.target.files`, etc.
18
8
 
19
- ## Example Usage
9
+ | Native Event | React Handler | Type |
10
+ | ------------ | ------------- | ------------ |
11
+ | `blur` | `onBlur` | `FocusEvent` |
12
+ | `focus` | `onFocus` | `FocusEvent` |
13
+ | `input` | `onInput` | `Event` |
14
+ | `change` | `onChange` | `Event` |
20
15
 
21
16
  ```tsx
22
- import { useState } from "react";
23
- import { Dialog, Button } from "@/components/ui";
17
+ // Correct: native events
18
+ <Input
19
+ onInput={(e) => console.log((e.target as HTMLInputElement).value)}
20
+ onChange={(e) => console.log((e.target as HTMLInputElement).value)}
21
+ />
22
+
23
+ // Wrong: these events don't fire on form controls
24
+ <Input onInput={(e: CustomEvent) => console.log(e.detail.value)} />
25
+ ```
26
+
27
+ ## Overlay / Complex Components — Custom `wa-` Events
28
+
29
+ `wa-dialog`, `wa-drawer`, `wa-dropdown`, `wa-popup`, `wa-tooltip`, etc. emit **custom events** prefixed with `wa-`. Event handlers receive `CustomEvent` objects.
30
+
31
+ | Web Awesome Event | React Handler | Type |
32
+ | ----------------- | ------------- | ------------- |
33
+ | `wa-show` | `onShow` | `CustomEvent` |
34
+ | `wa-hide` | `onHide` | `CustomEvent` |
35
+ | `wa-after-show` | `onAfterShow` | `CustomEvent` |
36
+ | `wa-after-hide` | `onAfterHide` | `CustomEvent` |
37
+ | `wa-clear` | `onClear` | `CustomEvent` |
38
+ | `wa-invalid` | `onInvalid` | `CustomEvent` |
39
+
40
+ ```tsx
41
+ import { useState } from 'react';
42
+ import { Dialog, Button } from '@/components/ui';
24
43
 
25
44
  function Example() {
26
45
  const [open, setOpen] = useState(false);
@@ -31,7 +50,7 @@ function Example() {
31
50
  <Dialog
32
51
  open={open}
33
52
  onHide={() => setOpen(false)}
34
- onAfterShow={(e) => console.log("Dialog shown", e)}
53
+ onAfterShow={(e) => console.log('Dialog shown', e)}
35
54
  >
36
55
  Dialog content
37
56
  </Dialog>
@@ -141,14 +141,25 @@ const { forwardRef, useRef, useEffect } = React;
141
141
 
142
142
  ### 3. Event Naming Convention
143
143
 
144
- | Web Awesome | React Prop |
145
- | --------------- | ------------- |
146
- | `wa-show` | `onShow` |
147
- | `wa-hide` | `onHide` |
148
- | `wa-after-show` | `onAfterShow` |
149
- | `wa-blur` | `onBlur` |
150
- | `wa-focus` | `onFocus` |
151
- | `wa-change` | `onChange` |
144
+ **Form controls** (wa-button, wa-input, wa-number-input, wa-file-input, wa-textarea, wa-select, wa-checkbox, wa-switch) emit **native DOM events** — no `wa-` prefix:
145
+
146
+ | Native Event | React Prop |
147
+ | ------------ | ---------- |
148
+ | `blur` | `onBlur` |
149
+ | `focus` | `onFocus` |
150
+ | `input` | `onInput` |
151
+ | `change` | `onChange` |
152
+
153
+ **Overlay/complex components** (wa-dialog, wa-drawer, wa-dropdown, wa-popup, etc.) emit **custom `wa-` events**:
154
+
155
+ | Web Awesome Event | React Prop |
156
+ | ----------------- | ------------- |
157
+ | `wa-show` | `onShow` |
158
+ | `wa-hide` | `onHide` |
159
+ | `wa-after-show` | `onAfterShow` |
160
+ | `wa-after-hide` | `onAfterHide` |
161
+ | `wa-clear` | `onClear` |
162
+ | `wa-invalid` | `onInvalid` |
152
163
 
153
164
  ### 4. Always Cleanup Event Listeners
154
165
 
@@ -82,12 +82,12 @@ export const Button = React.forwardRef(
82
82
  if (onFocus) onFocus(e);
83
83
  };
84
84
 
85
- el.addEventListener('wa-blur', handleBlur);
86
- el.addEventListener('wa-focus', handleFocus);
85
+ el.addEventListener('blur', handleBlur);
86
+ el.addEventListener('focus', handleFocus);
87
87
 
88
88
  return () => {
89
- el.removeEventListener('wa-blur', handleBlur);
90
- el.removeEventListener('wa-focus', handleFocus);
89
+ el.removeEventListener('blur', handleBlur);
90
+ el.removeEventListener('focus', handleFocus);
91
91
  };
92
92
  }, [onBlur, onFocus]);
93
93
 
@@ -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
  );