@marianmeres/stuic 2.5.0 → 2.6.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.
@@ -0,0 +1,83 @@
1
+ # Button
2
+
3
+ A flexible button component with style variants, sizes, and optional toggle/switch behavior. Can render as a button or anchor tag.
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ |------|------|---------|-------------|
9
+ | `variant` | `"primary" \| "secondary" \| string` | - | Style variant |
10
+ | `size` | `"sm" \| "md" \| "lg" \| string` | - | Button size |
11
+ | `muted` | `boolean` | `false` | Reduce text contrast for less emphasis |
12
+ | `noshadow` | `boolean` | `false` | Remove shadow effect |
13
+ | `noborder` | `boolean` | `false` | Remove border |
14
+ | `unstyled` | `boolean` | `false` | Skip all default styling |
15
+ | `inverse` | `boolean` | `false` | Transparent bg, styled on hover |
16
+ | `href` | `string` | - | Render as anchor tag with this URL |
17
+ | `roleSwitch` | `boolean` | `false` | Enable toggle/switch behavior |
18
+ | `checked` | `boolean` | `false` | Toggle state when `roleSwitch` is true (bindable) |
19
+ | `el` | `Element` | - | Element reference (bindable) |
20
+ | `class` | `string` | - | Additional CSS classes |
21
+
22
+ ## Snippet Props
23
+
24
+ The `children` snippet receives `{ checked }` when `roleSwitch` is enabled.
25
+
26
+ ## Usage
27
+
28
+ ### Basic Variants
29
+
30
+ ```svelte
31
+ <script lang="ts">
32
+ import { Button } from 'stuic';
33
+ </script>
34
+
35
+ <Button>Default</Button>
36
+ <Button variant="primary">Primary</Button>
37
+ <Button variant="secondary">Secondary</Button>
38
+ <Button size="sm">Small</Button>
39
+ <Button size="lg">Large</Button>
40
+ ```
41
+
42
+ ### Inverse Style (Ghost Button)
43
+
44
+ ```svelte
45
+ <Button inverse>
46
+ Hover to see background
47
+ </Button>
48
+ ```
49
+
50
+ ### As Link
51
+
52
+ ```svelte
53
+ <Button href="/dashboard">
54
+ Go to Dashboard
55
+ </Button>
56
+ ```
57
+
58
+ ### Toggle Button
59
+
60
+ ```svelte
61
+ <script lang="ts">
62
+ import { Button } from 'stuic';
63
+
64
+ let isActive = $state(false);
65
+ </script>
66
+
67
+ <Button roleSwitch bind:checked={isActive}>
68
+ {#snippet children({ checked })}
69
+ {checked ? 'ON' : 'OFF'}
70
+ {/snippet}
71
+ </Button>
72
+ ```
73
+
74
+ ### Custom Styling
75
+
76
+ ```svelte
77
+ <Button
78
+ unstyled
79
+ class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
80
+ >
81
+ Fully Custom
82
+ </Button>
83
+ ```
@@ -0,0 +1,94 @@
1
+ # ButtonGroupRadio
2
+
3
+ A radio button group styled as a segmented button toggle. Supports keyboard navigation and async validation.
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ |------|------|---------|-------------|
9
+ | `options` | `(string \| FieldRadiosOption)[]` | - | Array of options |
10
+ | `value` | `string` | - | Selected value (bindable) |
11
+ | `activeIndex` | `number` | - | Selected index (bindable) |
12
+ | `size` | `"sm" \| "md" \| "lg" \| string` | `"md"` | Button size |
13
+ | `disabled` | `boolean` | `false` | Disable all buttons |
14
+ | `tabindex` | `number` | `0` | Tab index for active button |
15
+ | `class` | `string` | - | CSS for container |
16
+ | `classButton` | `string` | - | CSS for all buttons |
17
+ | `classButtonActive` | `string` | - | CSS for active button |
18
+ | `style` | `string` | - | Inline styles for container |
19
+ | `onButtonClick` | `(index, coll) => Promise<boolean> \| boolean` | - | Async validation hook (return `false` to prevent) |
20
+ | `buttonProps` | `(index, coll) => Record<string, any>` | - | Dynamic props per button |
21
+
22
+ ## Option Format
23
+
24
+ ```ts
25
+ // Simple string
26
+ 'Option A'
27
+
28
+ // Or object
29
+ {
30
+ label: 'Option A',
31
+ value: 'a' // optional, defaults to label
32
+ }
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### Basic
38
+
39
+ ```svelte
40
+ <script lang="ts">
41
+ import { ButtonGroupRadio } from 'stuic';
42
+
43
+ let selected = $state('monthly');
44
+ </script>
45
+
46
+ <ButtonGroupRadio
47
+ options={['daily', 'weekly', 'monthly']}
48
+ bind:value={selected}
49
+ />
50
+
51
+ <p>Selected: {selected}</p>
52
+ ```
53
+
54
+ ### With Object Options
55
+
56
+ ```svelte
57
+ <script lang="ts">
58
+ let plan = $state('pro');
59
+ </script>
60
+
61
+ <ButtonGroupRadio
62
+ options={[
63
+ { label: 'Free', value: 'free' },
64
+ { label: 'Pro', value: 'pro' },
65
+ { label: 'Enterprise', value: 'enterprise' }
66
+ ]}
67
+ bind:value={plan}
68
+ />
69
+ ```
70
+
71
+ ### With Async Validation
72
+
73
+ ```svelte
74
+ <script lang="ts">
75
+ let value = $state('a');
76
+ </script>
77
+
78
+ <ButtonGroupRadio
79
+ options={['a', 'b', 'c']}
80
+ bind:value
81
+ onButtonClick={async (index, coll) => {
82
+ // Return false to prevent selection
83
+ if (index === 2) {
84
+ alert('Option C is disabled');
85
+ return false;
86
+ }
87
+ }}
88
+ />
89
+ ```
90
+
91
+ ## Keyboard Navigation
92
+
93
+ - **Arrow Left/Up**: Select previous option
94
+ - **Arrow Right/Down**: Select next option
@@ -0,0 +1,70 @@
1
+ # Circle
2
+
3
+ An SVG circle progress indicator with configurable stroke, rotation, and animated transitions.
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ |------|------|---------|-------------|
9
+ | `completeness` | `number` | `1` | Progress value from 0 to 1 |
10
+ | `strokeWidth` | `number` | `10` | Stroke width in SVG units |
11
+ | `bgStrokeColor` | `string` | - | Background circle stroke color |
12
+ | `roundedEdges` | `boolean` | `true` | Use rounded stroke line caps |
13
+ | `rotate` | `number` | `0` | Rotation in degrees |
14
+ | `strokeWidthRatio` | `number` | `0` | Ratio for background stroke width |
15
+ | `animateCompletenessMs` | `number` | `0` | Transition duration for progress changes (ms) |
16
+ | `class` | `string` | - | CSS for container div |
17
+ | `style` | `string` | - | Inline styles for container |
18
+ | `circleClass` | `string` | - | CSS for SVG circle element |
19
+ | `circleStyle` | `string` | - | Inline styles for circle |
20
+
21
+ ## Usage
22
+
23
+ ### Basic Progress Circle
24
+
25
+ ```svelte
26
+ <script lang="ts">
27
+ import { Circle } from 'stuic';
28
+ </script>
29
+
30
+ <Circle completeness={0.75} class="size-16" />
31
+ ```
32
+
33
+ ### Animated Progress
34
+
35
+ ```svelte
36
+ <script lang="ts">
37
+ import { Circle } from 'stuic';
38
+
39
+ let progress = $state(0);
40
+
41
+ function startProgress() {
42
+ progress = 0;
43
+ const interval = setInterval(() => {
44
+ progress += 0.1;
45
+ if (progress >= 1) clearInterval(interval);
46
+ }, 200);
47
+ }
48
+ </script>
49
+
50
+ <Circle
51
+ completeness={progress}
52
+ animateCompletenessMs={200}
53
+ class="size-20"
54
+ />
55
+
56
+ <button onclick={startProgress}>Start</button>
57
+ ```
58
+
59
+ ### Custom Styling
60
+
61
+ ```svelte
62
+ <Circle
63
+ completeness={0.5}
64
+ strokeWidth={8}
65
+ rotate={-90}
66
+ bgStrokeColor="rgba(0,0,0,0.1)"
67
+ class="size-24"
68
+ circleClass="stroke-blue-500"
69
+ />
70
+ ```
@@ -0,0 +1,64 @@
1
+ # ColorScheme
2
+
3
+ Hydration components for dark mode support. Add/remove the `dark` class on the document root based on localStorage and optionally system preference.
4
+
5
+ ## Components
6
+
7
+ ### ColorSchemeLocal
8
+
9
+ Uses only the localStorage setting to determine dark mode. Does not check system preference.
10
+
11
+ ### ColorSchemeSystemAware
12
+
13
+ Checks localStorage first, then falls back to system preference (`prefers-color-scheme: dark`).
14
+
15
+ ## Props
16
+
17
+ Both components have **no props** - they are pure hydration components.
18
+
19
+ ## Storage Key
20
+
21
+ Both components use the localStorage key: `stuic-color-scheme`
22
+
23
+ - Value `"dark"` → adds `dark` class to `<html>`
24
+ - Any other value → removes `dark` class
25
+
26
+ ## Usage
27
+
28
+ ### System-Aware (Recommended)
29
+
30
+ ```svelte
31
+ <script lang="ts">
32
+ import { ColorSchemeSystemAware } from 'stuic';
33
+ </script>
34
+
35
+ <ColorSchemeSystemAware />
36
+ ```
37
+
38
+ ### Local Only
39
+
40
+ ```svelte
41
+ <script lang="ts">
42
+ import { ColorSchemeLocal } from 'stuic';
43
+ </script>
44
+
45
+ <ColorSchemeLocal />
46
+ ```
47
+
48
+ ### Toggle Dark Mode
49
+
50
+ ```svelte
51
+ <script lang="ts">
52
+ import { ColorSchemeSystemAware } from 'stuic';
53
+
54
+ function toggleDarkMode() {
55
+ const root = document.documentElement;
56
+ const isDark = root.classList.toggle('dark');
57
+ localStorage.setItem('stuic-color-scheme', isDark ? 'dark' : 'light');
58
+ }
59
+ </script>
60
+
61
+ <ColorSchemeSystemAware />
62
+
63
+ <button onclick={toggleDarkMode}>Toggle Dark Mode</button>
64
+ ```
@@ -0,0 +1,97 @@
1
+ # CommandMenu
2
+
3
+ A searchable command palette/menu modal for quick navigation and selection. Supports keyboard navigation, option grouping, and async search.
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ |------|------|---------|-------------|
9
+ | `value` | `any` | - | Selected item (bindable) |
10
+ | `q` | `string` | `""` | Search query (bindable) |
11
+ | `getOptions` | `(q: string, current: Item[]) => Promise<Item[]>` | - | Async function to fetch options |
12
+ | `renderOptionLabel` | `(item: Item) => string` | - | Custom label renderer |
13
+ | `renderOptionGroup` | `(s: string) => string` | - | Custom group label renderer |
14
+ | `itemIdPropName` | `string` | `"id"` | Property name for item ID |
15
+ | `t` | `TranslateFn` | - | Translation function for i18n |
16
+ | `notifications` | `NotificationsStack` | - | Notifications stack for errors |
17
+ | `searchPlaceholder` | `string` | `"Type to search..."` | Input placeholder |
18
+ | `noScrollLock` | `boolean` | `false` | Disable body scroll lock |
19
+ | `showAllOnEmptyQ` | `boolean` | `false` | Show all options when query is empty |
20
+ | `classOption` | `string` | - | CSS for option buttons |
21
+ | `classOptionActive` | `string` | - | CSS for active option |
22
+ | `input` | `HTMLInputElement` | - | Input element reference (bindable) |
23
+
24
+ ## Methods
25
+
26
+ | Method | Description |
27
+ |--------|-------------|
28
+ | `open(opener?)` | Open the command menu |
29
+ | `close()` | Close the command menu |
30
+
31
+ ## Usage
32
+
33
+ ### Basic
34
+
35
+ ```svelte
36
+ <script lang="ts">
37
+ import { CommandMenu } from 'stuic';
38
+
39
+ let menu: CommandMenu;
40
+ let selected = $state(null);
41
+
42
+ async function getOptions(q: string) {
43
+ const items = [
44
+ { id: 'dashboard', label: 'Dashboard' },
45
+ { id: 'settings', label: 'Settings' },
46
+ { id: 'profile', label: 'Profile' },
47
+ ];
48
+ return items.filter(i =>
49
+ i.label.toLowerCase().includes(q.toLowerCase())
50
+ );
51
+ }
52
+ </script>
53
+
54
+ <button onclick={() => menu.open()}>Open Menu (Cmd+K)</button>
55
+
56
+ <CommandMenu
57
+ bind:this={menu}
58
+ bind:value={selected}
59
+ {getOptions}
60
+ renderOptionLabel={(item) => item.label}
61
+ />
62
+
63
+ {#if selected}
64
+ <p>Selected: {selected.label}</p>
65
+ {/if}
66
+ ```
67
+
68
+ ### With Option Groups
69
+
70
+ ```svelte
71
+ <script lang="ts">
72
+ async function getOptions(q: string) {
73
+ return [
74
+ { id: 'new-file', label: 'New File', optgroup: 'file' },
75
+ { id: 'open-file', label: 'Open File', optgroup: 'file' },
76
+ { id: 'copy', label: 'Copy', optgroup: 'edit' },
77
+ { id: 'paste', label: 'Paste', optgroup: 'edit' },
78
+ ];
79
+ }
80
+ </script>
81
+
82
+ <CommandMenu
83
+ bind:this={menu}
84
+ {getOptions}
85
+ renderOptionLabel={(item) => item.label}
86
+ renderOptionGroup={(group) => group.toUpperCase()}
87
+ />
88
+ ```
89
+
90
+ ## Keyboard Navigation
91
+
92
+ - **Arrow Up/Down**: Navigate options
93
+ - **Cmd/Ctrl + Arrow Up**: Jump to first option
94
+ - **Cmd/Ctrl + Arrow Down**: Jump to last option
95
+ - **Enter**: Select active option
96
+ - **Escape**: Close menu (or clear input first)
97
+ - **Tab**: Focus back to search input
@@ -0,0 +1,91 @@
1
+ # DismissibleMessage
2
+
3
+ A dismissible alert/message component with color themes and slide transition.
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ |------|------|---------|-------------|
9
+ | `message` | `THC \| Error` | - | Message content (string, HTML, or Error object) |
10
+ | `theme` | `TW_COLORS` | - | Tailwind color theme (e.g., `"red"`, `"green"`, `"blue"`) |
11
+ | `forceAsHtml` | `boolean` | `true` | Render message as HTML |
12
+ | `duration` | `number` | `150` | Slide transition duration (ms) |
13
+ | `onDismiss` | `(() => void) \| null \| false` | - | Dismiss callback (set to `false` to hide X button) |
14
+ | `class` | `string` | - | CSS for container |
15
+ | `classContent` | `string` | - | CSS for content area |
16
+ | `classDismiss` | `string` | - | CSS for dismiss button |
17
+ | `classX` | `string` | - | CSS for X icon |
18
+
19
+ ## Usage
20
+
21
+ ### Basic Message
22
+
23
+ ```svelte
24
+ <script lang="ts">
25
+ import { DismissibleMessage } from 'stuic';
26
+
27
+ let message = $state('This is an important notice.');
28
+ </script>
29
+
30
+ <DismissibleMessage
31
+ {message}
32
+ onDismiss={() => message = ''}
33
+ />
34
+ ```
35
+
36
+ ### With Theme Colors
37
+
38
+ ```svelte
39
+ <script lang="ts">
40
+ import { DismissibleMessage } from 'stuic';
41
+
42
+ let error = $state('Something went wrong!');
43
+ let success = $state('Operation completed successfully.');
44
+ </script>
45
+
46
+ <DismissibleMessage
47
+ message={error}
48
+ theme="red"
49
+ onDismiss={() => error = ''}
50
+ />
51
+
52
+ <DismissibleMessage
53
+ message={success}
54
+ theme="green"
55
+ onDismiss={() => success = ''}
56
+ />
57
+ ```
58
+
59
+ ### Non-Dismissible
60
+
61
+ ```svelte
62
+ <DismissibleMessage
63
+ message="This message cannot be dismissed."
64
+ theme="blue"
65
+ onDismiss={false}
66
+ />
67
+ ```
68
+
69
+ ### With Error Object
70
+
71
+ ```svelte
72
+ <script lang="ts">
73
+ let error = $state<Error | null>(null);
74
+
75
+ function doSomething() {
76
+ try {
77
+ throw new Error('Network request failed');
78
+ } catch (e) {
79
+ error = e as Error;
80
+ }
81
+ }
82
+ </script>
83
+
84
+ {#if error}
85
+ <DismissibleMessage
86
+ message={error}
87
+ theme="red"
88
+ onDismiss={() => error = null}
89
+ />
90
+ {/if}
91
+ ```
@@ -0,0 +1,110 @@
1
+ # Drawer
2
+
3
+ A slide-out panel component with configurable position, fly animation, and backdrop support.
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ |------|------|---------|-------------|
9
+ | `visible` | `boolean` | `false` | Controls visibility (bindable) |
10
+ | `position` | `"left" \| "top" \| "right" \| "bottom"` | `"left"` | Slide direction |
11
+ | `transitionDuration` | `number` | `200` | Animation duration (ms) |
12
+ | `animOffset` | `string \| number` | `"75vw"` | Fly animation distance |
13
+ | `focusTrap` | `boolean \| FocusTrapOptions` | - | Enable focus trapping |
14
+ | `onEscape` | `() => void` | - | Callback on Escape key |
15
+ | `onOutside` | `() => void` | - | Callback on backdrop click |
16
+ | `noBackdropScrollLock` | `boolean` | `false` | Disable body scroll lock |
17
+ | `class` | `string` | - | CSS for drawer panel |
18
+ | `classBackdrop` | `string` | - | CSS for backdrop |
19
+ | `labelledby` | `string` | - | ARIA labelledby ID |
20
+ | `describedby` | `string` | - | ARIA describedby ID |
21
+ | `el` | `HTMLDivElement` | - | Panel element reference (bindable) |
22
+ | `elBackdrop` | `HTMLDivElement` | - | Backdrop element reference (bindable) |
23
+
24
+ ## Methods
25
+
26
+ | Method | Description |
27
+ |--------|-------------|
28
+ | `open(opener?)` | Open drawer, optionally track opener for focus return |
29
+ | `close()` | Close drawer |
30
+ | `setOpener(el)` | Set element to refocus when closed |
31
+
32
+ ## Usage
33
+
34
+ ### Basic Side Drawer
35
+
36
+ ```svelte
37
+ <script lang="ts">
38
+ import { Drawer } from 'stuic';
39
+
40
+ let drawer: Drawer;
41
+ </script>
42
+
43
+ <button onclick={(e) => drawer.open(e)}>Open Drawer</button>
44
+
45
+ <Drawer
46
+ bind:this={drawer}
47
+ onEscape={() => drawer.close()}
48
+ onOutside={() => drawer.close()}
49
+ class="bg-white dark:bg-neutral-900 max-w-md"
50
+ >
51
+ <div class="p-6">
52
+ <h2>Drawer Content</h2>
53
+ <p>This is a side drawer.</p>
54
+ <button onclick={() => drawer.close()}>Close</button>
55
+ </div>
56
+ </Drawer>
57
+ ```
58
+
59
+ ### Right-Side Drawer
60
+
61
+ ```svelte
62
+ <Drawer
63
+ bind:this={drawer}
64
+ position="right"
65
+ onEscape={() => drawer.close()}
66
+ onOutside={() => drawer.close()}
67
+ class="bg-white dark:bg-neutral-900 max-w-sm"
68
+ >
69
+ <nav class="p-4">
70
+ <ul>
71
+ <li>Menu Item 1</li>
72
+ <li>Menu Item 2</li>
73
+ </ul>
74
+ </nav>
75
+ </Drawer>
76
+ ```
77
+
78
+ ### Bottom Sheet
79
+
80
+ ```svelte
81
+ <Drawer
82
+ bind:this={drawer}
83
+ position="bottom"
84
+ animOffset="50vh"
85
+ onEscape={() => drawer.close()}
86
+ class="bg-white dark:bg-neutral-900 max-h-[50vh]"
87
+ >
88
+ <div class="p-6">
89
+ Bottom sheet content
90
+ </div>
91
+ </Drawer>
92
+ ```
93
+
94
+ ### With Visible Binding
95
+
96
+ ```svelte
97
+ <script lang="ts">
98
+ let visible = $state(false);
99
+ </script>
100
+
101
+ <button onclick={() => visible = true}>Open</button>
102
+
103
+ <Drawer
104
+ bind:visible
105
+ onEscape={() => visible = false}
106
+ onOutside={() => visible = false}
107
+ >
108
+ <div class="p-4">Content</div>
109
+ </Drawer>
110
+ ```
@@ -0,0 +1,81 @@
1
+ # HoverExpandableWidth
2
+
3
+ A container that expands its width on hover, transitioning from a fixed width to a target width. Useful for sidebars or navigation that expand to show more content.
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ |------|------|---------|-------------|
9
+ | `enabled` | `boolean` | `true` (if fine pointer) | Enable hover expansion |
10
+ | `targetWidth` | `number` | `256` | Expanded width in pixels |
11
+ | `duration` | `number` | `150` | Transition duration (ms) |
12
+ | `delayIn` | `number` | `500` | Delay before expanding (ms) |
13
+ | `delayOut` | `number` | `300` | Delay before shrinking (ms) |
14
+ | `shadowOpacity` | `number` | `0.5` | Box shadow opacity when expanded |
15
+ | `class` | `string` | - | CSS classes for container |
16
+
17
+ ## Snippet Props
18
+
19
+ The `children` snippet receives state information:
20
+
21
+ | Prop | Type | Description |
22
+ |------|------|-------------|
23
+ | `isExpanded` | `boolean` | Currently expanded |
24
+ | `isExpanding` | `boolean` | Transition to expanded |
25
+ | `isShrinking` | `boolean` | Transition to collapsed |
26
+ | `inTransition` | `boolean` | Any transition active |
27
+
28
+ ## Usage
29
+
30
+ ### Basic Expandable Sidebar
31
+
32
+ ```svelte
33
+ <script lang="ts">
34
+ import { HoverExpandableWidth } from 'stuic';
35
+ </script>
36
+
37
+ <div class="w-16 h-screen">
38
+ <HoverExpandableWidth targetWidth={256}>
39
+ {#snippet children({ isExpanded })}
40
+ <nav class="h-full bg-gray-100 p-2">
41
+ {#if isExpanded}
42
+ <ul>
43
+ <li>Dashboard</li>
44
+ <li>Settings</li>
45
+ <li>Profile</li>
46
+ </ul>
47
+ {:else}
48
+ <ul>
49
+ <li>D</li>
50
+ <li>S</li>
51
+ <li>P</li>
52
+ </ul>
53
+ {/if}
54
+ </nav>
55
+ {/snippet}
56
+ </HoverExpandableWidth>
57
+ </div>
58
+ ```
59
+
60
+ ### With Custom Delays
61
+
62
+ ```svelte
63
+ <HoverExpandableWidth
64
+ targetWidth={320}
65
+ delayIn={300}
66
+ delayOut={200}
67
+ duration={200}
68
+ >
69
+ {#snippet children({ isExpanded, inTransition })}
70
+ <div class={inTransition ? 'opacity-50' : ''}>
71
+ {isExpanded ? 'Expanded content...' : 'Icon'}
72
+ </div>
73
+ {/snippet}
74
+ </HoverExpandableWidth>
75
+ ```
76
+
77
+ ## Notes
78
+
79
+ - Respects `prefers-reduced-motion` - animations disabled when user prefers reduced motion
80
+ - Automatically disabled on touch devices (coarse pointer)
81
+ - Uses `position: fixed` during expansion for overlay effect