svelte-p5-components 0.4.1 → 0.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.
package/README.md CHANGED
@@ -8,6 +8,16 @@ Higher-level Svelte 5 components built on [`svelte-p5`](../core). The pieces tha
8
8
  pnpm add svelte-p5 svelte-p5-components p5
9
9
  ```
10
10
 
11
+ To test a change that's merged on `main` but not yet released, install the preview build from [pkg.pr.new](https://pkg.pr.new/):
12
+
13
+ ```bash
14
+ pnpm add \
15
+ https://pkg.pr.new/edw1nzhao/svelte-p5/svelte-p5@main \
16
+ https://pkg.pr.new/edw1nzhao/svelte-p5/svelte-p5-components@main
17
+ ```
18
+
19
+ Pin to a SHA (`@<commit-sha>`) instead of `@main` for reproducible installs.
20
+
11
21
  ## `<Sketch>`
12
22
 
13
23
  `<P5Canvas>` plus a `ResizeObserver` on the parent element and automatic `pixelDensity(devicePixelRatio)`. The default you probably want.
@@ -31,6 +41,16 @@ pnpm add svelte-p5 svelte-p5-components p5
31
41
  </div>
32
42
  ```
33
43
 
44
+ Props:
45
+
46
+ | Prop | Type | Default | Notes |
47
+ | ---------- | ------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
+ | `sketch` | `SketchFn<Ext>` | — | Required. Generic over custom instance members (see the core README). |
49
+ | `hidpi` | `boolean \| number` | `true` | `true` → `devicePixelRatio`, `false` → `pixelDensity(1)`, number → that exact density (cap for large WEBGL canvases: `Math.min(devicePixelRatio, 2)`). |
50
+ | `instance` | `ExtendedP5<Ext> \| null` (bindable) | `null` | The p5 instance once mounted. |
51
+ | `onReady` | `(instance) => void` | — | Fires once after creation, density, and initial sizing. |
52
+ | `onResize` | `(instance, width, height) => void` | — | Fires after each `ResizeObserver`-driven `resizeCanvas` (not the initial sizing). Rebuild size-dependent state here; call `instance.loop()` if you're noLoop-gated. |
53
+
34
54
  ## `<FPSMonitor>`
35
55
 
36
56
  Absolute-positioned FPS readout. Takes the p5 instance and samples `p.frameRate()` every 30 frames by default.
@@ -0,0 +1,227 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte';
3
+
4
+ /**
5
+ * One item rendered as an icon button in the rail.
6
+ */
7
+ export interface ActivityBarItem {
8
+ /** Stable id. Passed back via `onSelect` and used for `activeId`. */
9
+ id: string;
10
+ /** Snippet that renders the icon. Called with no arguments. */
11
+ icon: Snippet;
12
+ /** Accessible label + tooltip title. */
13
+ label: string;
14
+ /** Optional numeric badge rendered over the icon. */
15
+ badge?: number;
16
+ }
17
+ </script>
18
+
19
+ <script lang="ts">
20
+ /**
21
+ * Vertical activity-bar rail (VS Code pattern). Renders a stack of
22
+ * square icon buttons wired into a `role="tablist"` group. One item
23
+ * can be "active"; callers own that state via the `activeId` prop.
24
+ *
25
+ * Visual contract: 48×48 square buttons. Active state renders an
26
+ * accent-colored left border + subtle background tint. Hover is a
27
+ * faint background. All class names are author-written (not scoped
28
+ * hashes) so consumers can safely target them via `:global(...)`:
29
+ *
30
+ * .activity-bar
31
+ * .activity-bar__item
32
+ * .activity-bar__item--active
33
+ * .activity-bar__badge
34
+ *
35
+ * Keyboard: Up/Down arrow moves focus between buttons (wraps); Enter
36
+ * or Space activates the focused item.
37
+ *
38
+ * @example
39
+ * ```svelte
40
+ * <ActivityBar
41
+ * activeId={active}
42
+ * items={[
43
+ * { id: 'viz', label: 'Visualizations', icon: vizIcon },
44
+ * { id: 'filters', label: 'Filters', icon: filtersIcon }
45
+ * ]}
46
+ * onSelect={(id) => (active = active === id ? undefined : id)}
47
+ * />
48
+ * ```
49
+ */
50
+ interface Props {
51
+ /** The items to render. */
52
+ items: ActivityBarItem[];
53
+ /**
54
+ * The id of the currently-active item. When undefined, no item
55
+ * shows the active treatment. Bindable.
56
+ */
57
+ activeId?: string;
58
+ /** Click / keyboard-activation callback. */
59
+ onSelect?: (id: string) => void;
60
+ /** Rail width in px. Default: 48. */
61
+ width?: number;
62
+ /**
63
+ * Rail orientation. Only `'vertical'` is supported today; the prop
64
+ * is kept so future work can add horizontal without breaking API.
65
+ */
66
+ orientation?: 'vertical';
67
+ class?: string;
68
+ }
69
+
70
+ let {
71
+ items,
72
+ activeId = $bindable<string | undefined>(undefined),
73
+ onSelect,
74
+ width = 48,
75
+ orientation = 'vertical',
76
+ class: className = ''
77
+ }: Props = $props();
78
+
79
+ let rootEl: HTMLDivElement | null = $state(null);
80
+
81
+ function handleItemClick(id: string) {
82
+ onSelect?.(id);
83
+ }
84
+
85
+ function handleKeydown(event: KeyboardEvent) {
86
+ if (!rootEl) return;
87
+ const buttons = Array.from(rootEl.querySelectorAll<HTMLButtonElement>('.activity-bar__item'));
88
+ if (buttons.length === 0) return;
89
+
90
+ const current = document.activeElement as HTMLElement | null;
91
+ const idx = current ? buttons.indexOf(current as HTMLButtonElement) : -1;
92
+
93
+ if (event.key === 'ArrowDown' || (orientation === 'vertical' && event.key === 'ArrowRight')) {
94
+ event.preventDefault();
95
+ const next = idx < 0 ? 0 : (idx + 1) % buttons.length;
96
+ buttons[next]?.focus();
97
+ } else if (
98
+ event.key === 'ArrowUp' ||
99
+ (orientation === 'vertical' && event.key === 'ArrowLeft')
100
+ ) {
101
+ event.preventDefault();
102
+ const next = idx < 0 ? buttons.length - 1 : (idx - 1 + buttons.length) % buttons.length;
103
+ buttons[next]?.focus();
104
+ } else if (event.key === 'Enter' || event.key === ' ') {
105
+ if (idx >= 0) {
106
+ event.preventDefault();
107
+ const id = items[idx]?.id;
108
+ if (id) onSelect?.(id);
109
+ }
110
+ }
111
+ }
112
+ </script>
113
+
114
+ <div
115
+ bind:this={rootEl}
116
+ class="activity-bar {className}"
117
+ role="tablist"
118
+ aria-orientation={orientation}
119
+ style:width="{width}px"
120
+ onkeydown={handleKeydown}
121
+ tabindex="-1"
122
+ >
123
+ {#each items as item (item.id)}
124
+ {@const isActive = item.id === activeId}
125
+ <button
126
+ type="button"
127
+ class="activity-bar__item {isActive ? 'activity-bar__item--active' : ''}"
128
+ role="tab"
129
+ aria-selected={isActive}
130
+ aria-label={item.label}
131
+ title={item.label}
132
+ tabindex={isActive ? 0 : -1}
133
+ onclick={() => handleItemClick(item.id)}
134
+ >
135
+ <span class="activity-bar__icon">
136
+ {@render item.icon()}
137
+ </span>
138
+ {#if item.badge !== undefined && item.badge > 0}
139
+ <span class="activity-bar__badge" aria-hidden="true">{item.badge}</span>
140
+ {/if}
141
+ </button>
142
+ {/each}
143
+ </div>
144
+
145
+ <style>
146
+ .activity-bar {
147
+ display: flex;
148
+ flex-direction: column;
149
+ align-items: stretch;
150
+ flex: 0 0 auto;
151
+ height: 100%;
152
+ background: var(--activity-bar-bg, #f3f3f3);
153
+ border-right: 1px solid var(--activity-bar-border, rgba(0, 0, 0, 0.08));
154
+ overflow: hidden;
155
+ }
156
+
157
+ .activity-bar__item {
158
+ position: relative;
159
+ display: inline-flex;
160
+ align-items: center;
161
+ justify-content: center;
162
+ width: 48px;
163
+ height: 48px;
164
+ padding: 0;
165
+ margin: 0;
166
+ border: none;
167
+ background: transparent;
168
+ color: var(--activity-bar-fg, #555);
169
+ cursor: pointer;
170
+ transition:
171
+ background-color 120ms ease,
172
+ color 120ms ease;
173
+ }
174
+
175
+ .activity-bar__item:hover {
176
+ background: var(--activity-bar-bg-hover, rgba(0, 0, 0, 0.05));
177
+ color: var(--activity-bar-fg-hover, #111);
178
+ }
179
+
180
+ .activity-bar__item:focus-visible {
181
+ outline: 2px solid var(--activity-bar-focus, rgba(59, 130, 246, 0.55));
182
+ outline-offset: -2px;
183
+ }
184
+
185
+ .activity-bar__item--active {
186
+ color: var(--activity-bar-fg-active, #111);
187
+ background: var(--activity-bar-bg-active, rgba(59, 130, 246, 0.08));
188
+ }
189
+
190
+ .activity-bar__item--active::before {
191
+ content: '';
192
+ position: absolute;
193
+ left: 0;
194
+ top: 0;
195
+ bottom: 0;
196
+ width: 2px;
197
+ background: var(--activity-bar-accent, #3b82f6);
198
+ }
199
+
200
+ .activity-bar__icon {
201
+ display: inline-flex;
202
+ align-items: center;
203
+ justify-content: center;
204
+ width: 22px;
205
+ height: 22px;
206
+ pointer-events: none;
207
+ }
208
+
209
+ .activity-bar__badge {
210
+ position: absolute;
211
+ top: 6px;
212
+ right: 6px;
213
+ min-width: 16px;
214
+ height: 16px;
215
+ padding: 0 4px;
216
+ border-radius: 8px;
217
+ background: var(--activity-bar-badge-bg, #ef4444);
218
+ color: var(--activity-bar-badge-fg, #fff);
219
+ font:
220
+ 10px/16px system-ui,
221
+ -apple-system,
222
+ Segoe UI,
223
+ sans-serif;
224
+ font-weight: 600;
225
+ text-align: center;
226
+ }
227
+ </style>
@@ -0,0 +1,66 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * One item rendered as an icon button in the rail.
4
+ */
5
+ export interface ActivityBarItem {
6
+ /** Stable id. Passed back via `onSelect` and used for `activeId`. */
7
+ id: string;
8
+ /** Snippet that renders the icon. Called with no arguments. */
9
+ icon: Snippet;
10
+ /** Accessible label + tooltip title. */
11
+ label: string;
12
+ /** Optional numeric badge rendered over the icon. */
13
+ badge?: number;
14
+ }
15
+ /**
16
+ * Vertical activity-bar rail (VS Code pattern). Renders a stack of
17
+ * square icon buttons wired into a `role="tablist"` group. One item
18
+ * can be "active"; callers own that state via the `activeId` prop.
19
+ *
20
+ * Visual contract: 48×48 square buttons. Active state renders an
21
+ * accent-colored left border + subtle background tint. Hover is a
22
+ * faint background. All class names are author-written (not scoped
23
+ * hashes) so consumers can safely target them via `:global(...)`:
24
+ *
25
+ * .activity-bar
26
+ * .activity-bar__item
27
+ * .activity-bar__item--active
28
+ * .activity-bar__badge
29
+ *
30
+ * Keyboard: Up/Down arrow moves focus between buttons (wraps); Enter
31
+ * or Space activates the focused item.
32
+ *
33
+ * @example
34
+ * ```svelte
35
+ * <ActivityBar
36
+ * activeId={active}
37
+ * items={[
38
+ * { id: 'viz', label: 'Visualizations', icon: vizIcon },
39
+ * { id: 'filters', label: 'Filters', icon: filtersIcon }
40
+ * ]}
41
+ * onSelect={(id) => (active = active === id ? undefined : id)}
42
+ * />
43
+ * ```
44
+ */
45
+ interface Props {
46
+ /** The items to render. */
47
+ items: ActivityBarItem[];
48
+ /**
49
+ * The id of the currently-active item. When undefined, no item
50
+ * shows the active treatment. Bindable.
51
+ */
52
+ activeId?: string;
53
+ /** Click / keyboard-activation callback. */
54
+ onSelect?: (id: string) => void;
55
+ /** Rail width in px. Default: 48. */
56
+ width?: number;
57
+ /**
58
+ * Rail orientation. Only `'vertical'` is supported today; the prop
59
+ * is kept so future work can add horizontal without breaking API.
60
+ */
61
+ orientation?: 'vertical';
62
+ class?: string;
63
+ }
64
+ declare const ActivityBar: import("svelte").Component<Props, {}, "activeId">;
65
+ type ActivityBar = ReturnType<typeof ActivityBar>;
66
+ export default ActivityBar;