@camera.ui/sdk 0.0.4 → 0.0.7

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.
@@ -547,8 +547,9 @@ interface PluginContract {
547
547
  */
548
548
  pythonVersion?: PythonVersion;
549
549
  /**
550
- * Extra package dependencies installed into the plugin's runtime (PyPI
551
- * for Python plugins, npm for Node plugins).
550
+ * Extra package dependencies installed into the plugin's runtime (Go
551
+ * module paths for Go plugins; PyPI / npm names for Python and Node
552
+ * plugins).
552
553
  */
553
554
  dependencies?: string[];
554
555
  }
@@ -5,7 +5,6 @@
5
5
  * ReplaySubject) and a small set of composable operators for building
6
6
  * property-change notifications and event streams throughout the SDK.
7
7
  */
8
- // ── Disposable ───────────────────────────────────────────────────────
9
8
  /**
10
9
  * Subscription handle returned by `subscribe()`.
11
10
  *
@@ -32,7 +31,6 @@ export class Disposable {
32
31
  this.dispose();
33
32
  }
34
33
  }
35
- // ── Observable ───────────────────────────────────────────────────────
36
34
  /**
37
35
  * Cold producer of a push-based value stream.
38
36
  *
@@ -74,7 +72,6 @@ export class Observable {
74
72
  return result;
75
73
  }
76
74
  }
77
- // ── Subject ──────────────────────────────────────────────────────────
78
75
  /**
79
76
  * Multicast value source.
80
77
  *
@@ -85,6 +82,7 @@ export class Observable {
85
82
  */
86
83
  export class Subject {
87
84
  #subscribers = new Set();
85
+ #completeHandlers = new Set();
88
86
  #completed = false;
89
87
  get closed() {
90
88
  return this.#completed;
@@ -101,6 +99,31 @@ export class Subject {
101
99
  return;
102
100
  this.#completed = true;
103
101
  this.#subscribers.clear();
102
+ const handlers = [...this.#completeHandlers];
103
+ this.#completeHandlers.clear();
104
+ for (const handler of handlers) {
105
+ handler();
106
+ }
107
+ }
108
+ /**
109
+ * Register a handler invoked once when this Subject completes. Runs
110
+ * immediately if already completed. Used by {@link firstValueFrom}.
111
+ *
112
+ * @internal
113
+ *
114
+ * @param handler - Completion callback.
115
+ *
116
+ * @returns Disposable that cancels the registration.
117
+ */
118
+ _onComplete(handler) {
119
+ if (this.#completed) {
120
+ handler();
121
+ return new Disposable(() => { });
122
+ }
123
+ this.#completeHandlers.add(handler);
124
+ return new Disposable(() => {
125
+ this.#completeHandlers.delete(handler);
126
+ });
104
127
  }
105
128
  subscribe(callback) {
106
129
  if (this.#completed) {
@@ -135,7 +158,6 @@ export class Subject {
135
158
  return new Observable((cb) => this.subscribe(cb));
136
159
  }
137
160
  }
138
- // ── BehaviorSubject ──────────────────────────────────────────────────
139
161
  /**
140
162
  * Subject seeded with an initial value that always remembers the latest
141
163
  * emission. New subscribers receive the current value immediately on
@@ -166,7 +188,6 @@ export class BehaviorSubject extends Subject {
166
188
  return disposable;
167
189
  }
168
190
  }
169
- // ── ReplaySubject ────────────────────────────────────────────────────
170
191
  /**
171
192
  * Subject that buffers up to the last `bufferSize` values (configurable,
172
193
  * defaults to unbounded). New subscribers immediately receive every
@@ -196,7 +217,6 @@ export class ReplaySubject extends Subject {
196
217
  return super.subscribe(callback);
197
218
  }
198
219
  }
199
- // ── Operators ────────────────────────────────────────────────────────
200
220
  /**
201
221
  * Emit only the values for which `predicate` returns `true`.
202
222
  *
@@ -372,11 +392,12 @@ export function share(config) {
372
392
  });
373
393
  };
374
394
  }
375
- // ── Utilities ────────────────────────────────────────────────────────
376
395
  /**
377
396
  * Subscribe to the source and return a Promise that resolves with its first
378
- * emitted value, then disposes the subscription. Rejects if the source has
379
- * already completed without emitting.
397
+ * emitted value, then disposes the subscription. Rejects if the source
398
+ * completes before emitting (Subject, BehaviorSubject, ReplaySubject). A bare
399
+ * Observable has no completion signal, so the Promise stays pending until it
400
+ * emits.
380
401
  *
381
402
  * @param observable - Source stream to await.
382
403
  *
@@ -392,28 +413,35 @@ export function share(config) {
392
413
  */
393
414
  export function firstValueFrom(observable) {
394
415
  return new Promise((resolve, reject) => {
395
- let resolved = false;
396
- // The callback may fire synchronously during subscribe() (BehaviorSubject/
397
- // ReplaySubject), so declare sub up front to avoid the temporal dead zone.
398
- let sub = undefined;
399
- sub = observable.subscribe((value) => {
400
- if (!resolved) {
401
- resolved = true;
402
- // sub may not be assigned yet if value is emitted synchronously (BehaviorSubject/ReplaySubject)
403
- if (sub) {
404
- sub.dispose();
405
- }
406
- resolve(value);
407
- }
416
+ let settled = false;
417
+ // Callbacks may fire synchronously during subscribe()/_onComplete()
418
+ // (BehaviorSubject/ReplaySubject/already-completed), so declare the
419
+ // handles up front and clean up both on settle.
420
+ let valueSub = undefined;
421
+ let completeSub = undefined;
422
+ const cleanup = () => {
423
+ valueSub?.dispose();
424
+ completeSub?.dispose();
425
+ };
426
+ valueSub = observable.subscribe((value) => {
427
+ if (settled)
428
+ return;
429
+ settled = true;
430
+ cleanup();
431
+ resolve(value);
408
432
  });
409
- // Already resolved synchronously — clean up immediately
410
- if (resolved) {
411
- sub.dispose();
433
+ if (settled) {
434
+ cleanup();
412
435
  return;
413
436
  }
414
- // If the observable completed without emitting (closed subject)
415
- if ('closed' in observable && observable.closed && !resolved) {
416
- reject(new Error('Observable completed without emitting a value'));
437
+ if (observable instanceof Subject) {
438
+ completeSub = observable._onComplete(() => {
439
+ if (settled)
440
+ return;
441
+ settled = true;
442
+ cleanup();
443
+ reject(new Error('Observable completed without emitting a value'));
444
+ });
417
445
  }
418
446
  });
419
447
  }
@@ -29,8 +29,6 @@
29
29
  * }
30
30
  * }
31
31
  * ```
32
- *
33
- * @see /examples/getting-started — end-to-end walkthrough.
34
32
  */
35
33
  export class BasePlugin {
36
34
  logger;
@@ -24,9 +24,13 @@ export var BatteryProperty;
24
24
  /** Battery charging state */
25
25
  export var ChargingState;
26
26
  (function (ChargingState) {
27
+ /** Device has no rechargeable battery */
27
28
  ChargingState["NotChargeable"] = "NOT_CHARGEABLE";
29
+ /** Battery is not charging */
28
30
  ChargingState["NotCharging"] = "NOT_CHARGING";
31
+ /** Battery is currently charging */
29
32
  ChargingState["Charging"] = "CHARGING";
33
+ /** Battery is fully charged */
30
34
  ChargingState["Full"] = "FULL";
31
35
  })(ChargingState || (ChargingState = {}));
32
36
  /**
@@ -2,10 +2,15 @@ import { Sensor, SensorType, SensorCategory } from './base.js';
2
2
  /** Security system arm/disarm states (HomeKit-compatible values) */
3
3
  export var SecuritySystemState;
4
4
  (function (SecuritySystemState) {
5
+ /** Armed, occupants home */
5
6
  SecuritySystemState[SecuritySystemState["StayArm"] = 0] = "StayArm";
7
+ /** Armed, occupants away */
6
8
  SecuritySystemState[SecuritySystemState["AwayArm"] = 1] = "AwayArm";
9
+ /** Armed for night mode */
7
10
  SecuritySystemState[SecuritySystemState["NightArm"] = 2] = "NightArm";
11
+ /** System disarmed */
8
12
  SecuritySystemState[SecuritySystemState["Disarmed"] = 3] = "Disarmed";
13
+ /** Alarm is triggered */
9
14
  SecuritySystemState[SecuritySystemState["AlarmTriggered"] = 4] = "AlarmTriggered";
10
15
  })(SecuritySystemState || (SecuritySystemState = {}));
11
16
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camera.ui/sdk",
3
- "version": "0.0.4",
3
+ "version": "0.0.7",
4
4
  "description": "camera.ui sdk",
5
5
  "exports": {
6
6
  ".": {
@@ -21,14 +21,13 @@
21
21
  "docs:build": "npm run predocs && rimraf docs/.vitepress/dist && vitepress build docs && cpy --flat docs/logo.png docs/.vitepress/dist",
22
22
  "docs:preview": "npm run predocs && vitepress preview docs --port 5274",
23
23
  "format": "prettier --write 'src/' --ignore-unknown --no-error-on-unmatched-pattern",
24
+ "install-updates": "npm i --save",
24
25
  "lint": "eslint .",
25
- "type-check": "tsc --noEmit --project tsconfig.node.json",
26
26
  "lint:fix": "eslint --fix .",
27
- "test": "vitest run",
28
- "update": "updates --update ./",
29
- "install-updates": "npm i --save",
30
27
  "predocs": "typedoc && node scripts/build-example-docs.mjs",
31
- "prepublishOnly": "npm i --package-lock-only && npm run lint:fix && npm run format && npm run build"
28
+ "prepublishOnly": "npm i --package-lock-only && npm run lint:fix && npm run format && npm run build",
29
+ "test": "vitest run",
30
+ "type-check": "tsc --noEmit --project tsconfig.node.json"
32
31
  },
33
32
  "devDependencies": {
34
33
  "@rollup/plugin-node-resolve": "^16.0.3",
@@ -36,22 +35,20 @@
36
35
  "@stylistic/eslint-plugin": "^5.10.0",
37
36
  "@types/multicast-dns": "^7.2.4",
38
37
  "@types/node": "25.9.3",
39
- "@typescript-eslint/parser": "^8.61.0",
38
+ "@typescript-eslint/parser": "^8.61.1",
40
39
  "cpy-cli": "^7.0.0",
41
40
  "eslint": "9.39.2",
42
- "eslint-plugin-jsdoc": "^63.0.2",
41
+ "eslint-plugin-jsdoc": "^63.0.6",
43
42
  "globals": "^17.6.0",
44
43
  "prettier": "^3.8.4",
45
44
  "rimraf": "^6.1.3",
46
45
  "rollup": "^4.62.0",
47
46
  "rollup-plugin-dts": "^6.4.1",
48
- "sharp": "^0.35.1",
49
47
  "tsyringe": "^4.10.0",
50
48
  "typedoc-plugin-markdown": "^4.12.0",
51
49
  "typedoc-vitepress-theme": "^1.1.3",
52
50
  "typescript": "5.9.3",
53
- "typescript-eslint": "^8.61.0",
54
- "updates": "^17.18.0",
51
+ "typescript-eslint": "^8.61.1",
55
52
  "vitepress": "^1.6.4",
56
53
  "vitest": "^4.1.9",
57
54
  "werift": "0.23.0",
package/CHANGELOG.md DELETED
@@ -1,8 +0,0 @@
1
- All notable changes to this project will be documented in this file.
2
-
3
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
4
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
-
6
- ## [X.X.X] - ???
7
-
8
- - Initial Release
package/CONTRIBUTING.md DELETED
@@ -1 +0,0 @@
1
- # Contributing
@@ -1,77 +0,0 @@
1
- import { createRequire } from 'node:module';
2
- import { defineConfig } from 'vitepress';
3
-
4
- const require = createRequire(import.meta.url);
5
- const typedocSidebar = require('../api/typedoc-sidebar.json');
6
-
7
- // https://vitepress.dev/reference/site-config
8
- export default defineConfig({
9
- srcDir: '.',
10
- base: '/sdk/node/',
11
- title: 'camera.ui Node SDK',
12
- description: 'TypeScript SDK for building camera.ui plugins',
13
- lastUpdated: true,
14
- head: [
15
- ['link', { rel: 'icon', type: 'image/x-icon', sizes: '32x32', href: '/sdk/node/favicon.ico' }],
16
- ['link', { rel: 'icon', type: 'image/x-icon', sizes: '16x16', href: '/sdk/node/favicon-16.ico' }],
17
- ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/sdk/node/apple-touch-icon.png' }],
18
- ['meta', { name: 'theme-color', content: '#df2a4c' }],
19
- ],
20
- themeConfig: {
21
- logo: '/logo.svg',
22
-
23
- // typedocSidebar entries are alphabetical:
24
- // [0] camera, [1] manager, [2] observable, [3] plugin,
25
- // [4] sensor, [5] storage, [6] types
26
- sidebar: [
27
- {
28
- text: 'Plugin API',
29
- items: typedocSidebar[3].items,
30
- },
31
- {
32
- text: 'Camera',
33
- items: typedocSidebar[0].items,
34
- },
35
- {
36
- text: 'Sensors',
37
- items: typedocSidebar[4].items,
38
- },
39
- {
40
- text: 'Storage & Schema',
41
- items: typedocSidebar[5].items,
42
- },
43
- {
44
- text: 'Manager',
45
- items: typedocSidebar[1].items,
46
- },
47
- {
48
- text: 'Observable',
49
- items: typedocSidebar[2].items,
50
- },
51
- {
52
- text: 'Types',
53
- items: typedocSidebar[6].items,
54
- },
55
- {
56
- text: 'Plugin Guide',
57
- link: '/examples/getting-started',
58
- },
59
- ],
60
-
61
- search: {
62
- provider: 'local',
63
- },
64
-
65
- socialLinks: [
66
- { icon: 'github', link: 'https://github.com/seydx/camera.ui' },
67
- { icon: 'discord', link: 'https://discord.gg/bBGnGcbz8N' },
68
- {
69
- icon: {
70
- svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="currentColor" d="M20 10.04a2.18 2.18 0 0 0-3.7-1.55a10.7 10.7 0 0 0-5.79-1.83l.99-4.65l3.23.69a1.55 1.55 0 1 0 .15-.74l-3.59-.76a.37.37 0 0 0-.44.28l-1.1 5.18a10.74 10.74 0 0 0-5.87 1.83a2.19 2.19 0 1 0-2.41 3.59a4 4 0 0 0-.05.66c0 3.36 3.91 6.09 8.74 6.09s8.74-2.73 8.74-6.09a4 4 0 0 0-.05-.66A2.19 2.19 0 0 0 20 10.04m-15 1.56a1.56 1.56 0 1 1 1.56 1.56A1.56 1.56 0 0 1 5 11.6m8.71 4.13c-1.07.81-2.39 1.21-3.71 1.16a5.85 5.85 0 0 1-3.71-1.16a.4.4 0 1 1 .55-.59A4.94 4.94 0 0 0 10 16.1a4.94 4.94 0 0 0 3.16-.96a.41.41 0 0 1 .57.04a.41.41 0 0 1-.02.55m-.16-2.55a1.56 1.56 0 1 1 1.56-1.56a1.56 1.56 0 0 1-1.55 1.56z"/></svg>',
71
- },
72
- link: 'https://www.reddit.com/r/cameraui/',
73
- ariaLabel: 'Reddit',
74
- },
75
- ],
76
- },
77
- });
@@ -1,5 +0,0 @@
1
- import DefaultTheme from 'vitepress/theme';
2
-
3
- import './style.css';
4
-
5
- export default DefaultTheme;
@@ -1,117 +0,0 @@
1
- @import 'vitepress/dist/client/theme-default/styles/vars.css';
2
-
3
- /* camera.ui brand colors — mirror ui/src/plugins/preset.ts */
4
- :root {
5
- --cui-primary-50: #fdf4f6;
6
- --cui-primary-100: #f7ccd4;
7
- --cui-primary-200: #f1a3b2;
8
- --cui-primary-300: #eb7b90;
9
- --cui-primary-400: #e5526e;
10
- --cui-primary-500: #df2a4c;
11
- --cui-primary-600: #be2441;
12
- --cui-primary-700: #9c1d35;
13
- --cui-primary-800: #7b172a;
14
- --cui-primary-900: #59111e;
15
- --cui-primary-950: #380b13;
16
- }
17
-
18
- :root {
19
- --vp-c-brand-1: var(--cui-primary-500);
20
- --vp-c-brand-2: var(--cui-primary-600);
21
- --vp-c-brand-3: var(--cui-primary-700);
22
- --vp-c-brand-soft: rgba(223, 42, 76, 0.14);
23
-
24
- --vp-c-default-1: var(--vp-c-gray-1);
25
- --vp-c-default-2: var(--vp-c-gray-2);
26
- --vp-c-default-3: var(--vp-c-gray-3);
27
- --vp-c-default-soft: var(--vp-c-gray-soft);
28
-
29
- --vp-button-brand-border: transparent;
30
- --vp-button-brand-text: #ffffff;
31
- --vp-button-brand-bg: var(--cui-primary-500);
32
- --vp-button-brand-hover-border: transparent;
33
- --vp-button-brand-hover-text: #ffffff;
34
- --vp-button-brand-hover-bg: var(--cui-primary-600);
35
- --vp-button-brand-active-border: transparent;
36
- --vp-button-brand-active-text: #ffffff;
37
- --vp-button-brand-active-bg: var(--cui-primary-700);
38
-
39
- --vp-home-hero-name-color: transparent;
40
- --vp-home-hero-name-background: linear-gradient(
41
- 135deg,
42
- var(--cui-primary-500) 0%,
43
- var(--cui-primary-700) 100%
44
- );
45
- --vp-home-hero-image-background-image: linear-gradient(
46
- 135deg,
47
- rgba(223, 42, 76, 0.32) 0%,
48
- rgba(123, 23, 42, 0.32) 100%
49
- );
50
- --vp-home-hero-image-filter: blur(56px);
51
-
52
- --vp-custom-block-tip-border: transparent;
53
- --vp-custom-block-tip-text: var(--cui-primary-700);
54
- --vp-custom-block-tip-bg: var(--vp-c-brand-soft);
55
- --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
56
- }
57
-
58
- /* dark surface palette mirrors ui preset.ts (neutral grey, not slate) */
59
- .dark {
60
- --cui-surface-0: #ffffff;
61
- --cui-surface-50: #f5f5f5;
62
- --cui-surface-100: #d1d1d1;
63
- --cui-surface-200: #adadad;
64
- --cui-surface-300: #898989;
65
- --cui-surface-400: #646464;
66
- --cui-surface-500: #404040;
67
- --cui-surface-600: #363636;
68
- --cui-surface-700: #2d2d2d;
69
- --cui-surface-800: #232323;
70
- --cui-surface-850: #1d1d1d;
71
- --cui-surface-900: #1a1a1a;
72
- --cui-surface-925: #161616;
73
- --cui-surface-950: #101010;
74
- --cui-surface-1000: #0d0d0d;
75
-
76
- --vp-c-brand-1: var(--cui-primary-400);
77
- --vp-c-brand-2: var(--cui-primary-300);
78
- --vp-c-brand-3: var(--cui-primary-200);
79
- --vp-c-brand-soft: rgba(229, 82, 110, 0.16);
80
-
81
- --vp-c-bg: var(--cui-surface-900);
82
- --vp-c-bg-alt: var(--cui-surface-950);
83
- --vp-c-bg-elv: var(--cui-surface-800);
84
- --vp-c-bg-soft: var(--cui-surface-850);
85
-
86
- --vp-c-divider: var(--cui-surface-800);
87
- --vp-c-gutter: var(--cui-surface-950);
88
- --vp-c-border: var(--cui-surface-700);
89
-
90
- --vp-c-default-1: var(--cui-surface-800);
91
- --vp-c-default-2: var(--cui-surface-700);
92
- --vp-c-default-3: var(--cui-surface-600);
93
- --vp-c-default-soft: rgba(64, 64, 64, 0.4);
94
-
95
- --vp-c-text-1: rgba(255, 255, 255, 0.92);
96
- --vp-c-text-2: rgba(255, 255, 255, 0.66);
97
- --vp-c-text-3: rgba(255, 255, 255, 0.42);
98
-
99
- --vp-code-bg: var(--cui-surface-925);
100
- --vp-code-block-bg: var(--cui-surface-925);
101
-
102
- --vp-button-brand-text: #ffffff;
103
- --vp-button-brand-bg: var(--cui-primary-500);
104
- --vp-button-brand-hover-bg: var(--cui-primary-400);
105
- --vp-button-brand-active-bg: var(--cui-primary-600);
106
-
107
- --vp-custom-block-tip-text: var(--cui-primary-200);
108
- }
109
-
110
- /* Pull search/box accents in line with brand */
111
- .dark .DocSearch {
112
- --docsearch-primary-color: var(--cui-primary-400);
113
- }
114
-
115
- .DocSearch {
116
- --docsearch-primary-color: var(--cui-primary-500);
117
- }
package/docs/index.md DELETED
@@ -1,16 +0,0 @@
1
- # camera.ui Node SDK
2
-
3
- TypeScript SDK for building plugins that extend [camera.ui](https://github.com/seydx/camera.ui) — add cameras, run motion / object / face / license-plate / audio detection, deliver notifications, and more.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install @camera.ui/sdk
9
- ```
10
-
11
- ## Next steps
12
-
13
- - **[Plugin Guide](/examples/getting-started)** — everything you need to ship a Node plugin in one page: contract, lifecycle, sensors, storage, optional interfaces.
14
- - **[API Reference](/api/)** — module-by-module reference auto-generated from JSDoc.
15
-
16
- For complete production plugins to study, see [`plugins/`](https://github.com/seydx/camera.ui/tree/main/plugins) in the camera.ui repo.
package/docs/logo.png DELETED
Binary file
Binary file
Binary file
Binary file
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 233.63 233.63"><defs><style>.cls-1{fill:url(#dark_wine);}.cls-2{fill:url(#light_wine);}.cls-3{fill:none;stroke:#000;stroke-miterlimit:10;opacity:0;}.cls-4{fill:#c5c6c8;}.cls-5{fill:#1a1e21;stroke:#3a3a3a;stroke-width:2px;}.cls-6{fill:#fff;}</style><linearGradient id="dark_wine" x1="87.36" y1="158.67" x2="87.36" y2="59.85" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#730000"/><stop offset="0.49" stop-color="#a51733"/></linearGradient><linearGradient id="light_wine" x1="113.06" y1="197.59" x2="113.06" y2="80.87" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f44a6b"/><stop offset="0.28" stop-color="#df294c"/></linearGradient></defs><g id="CameraUI_U"><path id="U_Left" class="cls-1" d="M47.05,119.47s7.58,16,14.12,23.42a45.28,45.28,0,0,0,16.37,11.86A49.46,49.46,0,0,0,99,158.62c9.43-.46,20.55-3.87,29.24-11.86-3.9,2.5-9.87,5.1-15.9,4.77a25.63,25.63,0,0,1-18.38-9.09c-9.39-10.31-7.62-28.35-7.75-32.68-.35-11.42.77-21.92-1.76-36.19-.46-2.64-1.79-5.19-4.55-8-1.83-1.86-8.4-5.75-13.42-5.74-7.31,0-12.95,3.71-15.38,7.84-2,3.41-3.06,4.22-4.23,25.08a217.23,217.23,0,0,0,.22,26.76Z" transform="translate(0.5 0.5)"/><path id="U_Right" class="cls-2" d="M144.12,89.6a16.24,16.24,0,0,1,4.06-4.9c4.16-3.4,9-3.67,10.92-3.82a19.7,19.7,0,0,1,12.53,4.58c1.19,1,6.11,5.58,7.13,17.29.73,8.51,3.31,65.93-37,87.14-4.22,2.22-18,9.25-35.66,7.38a62.43,62.43,0,0,1-20.8-6.11,65.8,65.8,0,0,1-18.89-14.23c-20.21-21.67-19.36-57.46-19.36-57.46s7.58,16,14.12,23.42a45.28,45.28,0,0,0,16.37,11.86A49.46,49.46,0,0,0,99,158.62c11.66-.57,25.93-5.64,35-18.32C143.52,126.8,138.46,100.51,144.12,89.6Z" transform="translate(0.5 0.5)"/></g><g id="CameraUI_Lens"><circle id="ellipse" class="cls-3" cx="116.81" cy="116.81" r="116.31"/><g id="lens"><circle id="lens_bg" class="cls-4" cx="159.13" cy="58.39" r="18.66"/><path id="lens_inner" class="cls-5" d="M169.64,58.31a11,11,0,1,1-22,0c0-6.08,4.93-11.86,11-11.86S169.64,52.23,169.64,58.31Z" transform="translate(0.5 0.5)"/><g id="reflections"><ellipse id="dot_1" class="cls-6" cx="174.36" cy="53.69" rx="1.84" ry="1.42"/><ellipse id="dot_2" class="cls-6" cx="174.36" cy="57.08" rx="1.84" ry="1.42"/><ellipse id="dot_3" class="cls-6" cx="174.36" cy="60.46" rx="1.84" ry="1.42"/><circle id="dot_4" class="cls-6" cx="164.09" cy="63.77" r="1.66"/><path id="wide_1" class="cls-6" d="M156.63,48.9c-1.07-.88-5.52,3.09-6.31,5.43a1.53,1.53,0,0,0,0,1.26c.43.69,1.73.61,2.19.54,2.68-.44,4.71-4.09,4.8-4.37A3.21,3.21,0,0,0,156.63,48.9Z" transform="translate(0.5 0.5)"/></g></g></g></svg>
@@ -1,7 +0,0 @@
1
- # Plugin Guide
2
-
3
- Everything you need to ship a camera.ui plugin in one page: contract, lifecycle, sensors, storage, optional interfaces (discovery, notifier, detection), and inter-plugin communication.
4
-
5
- [Read the guide →](./getting-started.md)
6
-
7
- For complete production plugins to read alongside, see [`plugins/`](https://github.com/seydx/camera.ui/tree/main/plugins) in the camera.ui repo.