ocwi-core 0.1.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/CHANGELOG.md +36 -0
- package/LICENSE.md +100 -0
- package/README.md +307 -0
- package/dist/ocwi.min.js +722 -0
- package/dist/ocwi.min.js.map +1 -0
- package/package.json +74 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to OCWI are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
6
|
+
OCWI uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [0.1.1] – 2025-02-26
|
|
11
|
+
|
|
12
|
+
### Initial public release
|
|
13
|
+
|
|
14
|
+
First publicly available version of OCWI.
|
|
15
|
+
|
|
16
|
+
#### Features
|
|
17
|
+
|
|
18
|
+
- Single-file IIFE bundle (`dist/ocwi.min.js`) — drop into any page via `<script>` tag
|
|
19
|
+
- Web Component (`<ocwi-chat>`) built with Lit, fully isolated in Shadow DOM
|
|
20
|
+
- Config layering: default → Luma remote → local overrides
|
|
21
|
+
- Luma integration: fetch remote UI config on init, cookie-based cache for instant load
|
|
22
|
+
- Dana integration: SSE streaming chat via Luma proxy; structured error event detection
|
|
23
|
+
- Automatic reconnect with exponential backoff when Dana or Luma is unavailable
|
|
24
|
+
- Connection status indicator (split dot: Luma left / Dana right)
|
|
25
|
+
- i18n: built-in `en`, `cs`, `de` dictionaries; runtime language switch; custom dictionary merge
|
|
26
|
+
- Full theming via CSS custom properties (`--ocwi-primary`, `--ocwi-bg`, etc.)
|
|
27
|
+
- Message actions: copy, edit (user), refresh (assistant)
|
|
28
|
+
- Configurable send keybind (`Enter`, `Shift+Enter`, `Ctrl+Enter`, `Alt+Enter`, `None`)
|
|
29
|
+
- Watermark support (image, repeat mode, position)
|
|
30
|
+
- Accessible: `aria-label`, keyboard navigation, focus management
|
|
31
|
+
- Mobile-responsive layout with safe-area inset support
|
|
32
|
+
|
|
33
|
+
#### API adapters
|
|
34
|
+
|
|
35
|
+
- `MockApi` — in-memory mock with simulated streaming (demo / development)
|
|
36
|
+
- `HttpApi` — real SSE streaming via `danaUrl`; SSE error detection; abort/stop support
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# OCWI Sustainable Use License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present, dana-luma-ocwi contributors
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
8
|
+
|
|
9
|
+
## Copyright License
|
|
10
|
+
|
|
11
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide,
|
|
12
|
+
non-sublicensable, non-transferable license to use, copy, distribute,
|
|
13
|
+
make available, and prepare derivative works of the software, in each
|
|
14
|
+
case subject to the limitations and conditions below.
|
|
15
|
+
|
|
16
|
+
## Limitations
|
|
17
|
+
|
|
18
|
+
**You may use or modify the software only for your own internal business
|
|
19
|
+
purposes, for non-commercial or personal use, or to embed the widget
|
|
20
|
+
into your own website or web application as a user-facing chat interface.**
|
|
21
|
+
|
|
22
|
+
**You may distribute the software or provide it to others only if you do
|
|
23
|
+
so free of charge and for non-commercial purposes.**
|
|
24
|
+
|
|
25
|
+
**You may not offer the software (or any substantially similar embeddable
|
|
26
|
+
AI chat widget product built on or derived from it) as a standalone
|
|
27
|
+
commercial product, hosted service, or SaaS offering — i.e. you may not
|
|
28
|
+
sell, license, or monetise the widget itself as the primary deliverable.**
|
|
29
|
+
|
|
30
|
+
You may not alter, remove, or obscure any licensing, copyright, or other
|
|
31
|
+
notices of the licensor in the software. Any use of the licensor's
|
|
32
|
+
trademarks is subject to applicable law.
|
|
33
|
+
|
|
34
|
+
## Patents
|
|
35
|
+
|
|
36
|
+
The licensor grants you a license, under any patent claims the licensor
|
|
37
|
+
can license, or becomes able to license, to make, have made, use, sell,
|
|
38
|
+
offer for sale, import and have imported the software, in each case
|
|
39
|
+
subject to the limitations and conditions in this license. This license
|
|
40
|
+
does not cover any patent claims that you cause to be infringed by
|
|
41
|
+
modifications or additions to the software. If you or your company make
|
|
42
|
+
any written claim that the software infringes or contributes to
|
|
43
|
+
infringement of any patent, your patent license for the software granted
|
|
44
|
+
under these terms ends immediately. If your company makes such a claim,
|
|
45
|
+
your patent license ends immediately for work on behalf of your company.
|
|
46
|
+
|
|
47
|
+
## Notices
|
|
48
|
+
|
|
49
|
+
You must ensure that anyone who gets a copy of any part of the software
|
|
50
|
+
from you also gets a copy of these terms. If you modify the software,
|
|
51
|
+
you must include in any modified copies of the software a prominent
|
|
52
|
+
notice stating that you have modified the software.
|
|
53
|
+
|
|
54
|
+
## No Other Rights
|
|
55
|
+
|
|
56
|
+
These terms do not imply any licenses other than those expressly granted
|
|
57
|
+
in these terms.
|
|
58
|
+
|
|
59
|
+
## Termination
|
|
60
|
+
|
|
61
|
+
If you use the software in violation of these terms, such use is not
|
|
62
|
+
licensed, and your license will automatically terminate. If the licensor
|
|
63
|
+
provides you with a notice of your violation, and you cease all violation
|
|
64
|
+
of this license no later than 30 days after you receive that notice, your
|
|
65
|
+
license will be reinstated retroactively. However, if you violate these
|
|
66
|
+
terms after such reinstatement, any additional violation of these terms
|
|
67
|
+
will cause your license to terminate automatically and permanently.
|
|
68
|
+
|
|
69
|
+
## No Warranty
|
|
70
|
+
|
|
71
|
+
As far as the law allows, the software comes as is, without any warranty
|
|
72
|
+
or condition, and the licensor will not be liable to you for any damages
|
|
73
|
+
arising out of these terms or the use or nature of the software, under
|
|
74
|
+
any kind of legal claim.
|
|
75
|
+
|
|
76
|
+
## Definitions
|
|
77
|
+
|
|
78
|
+
The **"licensor"** is the entity offering these terms.
|
|
79
|
+
|
|
80
|
+
The **"software"** is the OCWI project the licensor makes available under
|
|
81
|
+
these terms.
|
|
82
|
+
|
|
83
|
+
**"You"** refers to the individual or entity agreeing to these terms.
|
|
84
|
+
|
|
85
|
+
**"Your company"** is any legal entity, sole proprietorship, or other kind
|
|
86
|
+
of organization that you work for, plus all organizations that have
|
|
87
|
+
control over, are under the control of, or are under common control with
|
|
88
|
+
that organization.
|
|
89
|
+
|
|
90
|
+
**"Control"** means ownership of substantially all the assets of an entity,
|
|
91
|
+
or the power to direct its management and policies by vote, contract, or
|
|
92
|
+
otherwise. Control can be direct or indirect.
|
|
93
|
+
|
|
94
|
+
**"Your licenses"** are all the licenses granted to you for the software
|
|
95
|
+
under these terms.
|
|
96
|
+
|
|
97
|
+
**"Use"** means anything you do with the software requiring one of your
|
|
98
|
+
licenses.
|
|
99
|
+
|
|
100
|
+
**"Trademark"** means trademarks, service marks, and similar rights.
|
package/README.md
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# OCWI — Embeddable AI Chat Widget
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/ocwi)
|
|
4
|
+
[](LICENSE.md)
|
|
5
|
+
[](https://cdn.jsdelivr.net/npm/ocwi/dist/ocwi.min.js)
|
|
6
|
+
|
|
7
|
+
Framework-agnostic, single-file JS chat widget built with TypeScript and Lit.
|
|
8
|
+
Ships as one IIFE bundle — drop it into any web page with a single `<script>` tag.
|
|
9
|
+
|
|
10
|
+
**Key properties:**
|
|
11
|
+
|
|
12
|
+
- No React / Vue / Svelte — only [Lit](https://lit.dev/) (Web Components)
|
|
13
|
+
- Single JS file: `dist/ocwi.min.js` (Lit bundled in, ~50 kB gzip)
|
|
14
|
+
- Shadow DOM + CSS variables for full theme isolation
|
|
15
|
+
- Connects to **Luma** (config server) and streams AI responses from **Dana**
|
|
16
|
+
- Config layering: default → Luma remote → local overrides
|
|
17
|
+
- i18n with runtime language switch and custom dictionaries (`en`, `cs`, `de` built-in)
|
|
18
|
+
- Automatic reconnect with exponential backoff
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## How it works
|
|
23
|
+
|
|
24
|
+
OCWI is the browser-side piece of a three-component system:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
┌─────────────────────────────────────────────────────────┐
|
|
28
|
+
│ Browser │
|
|
29
|
+
│ │
|
|
30
|
+
│ ┌──────────────────────────────────────────────────┐ │
|
|
31
|
+
│ │ OCWI (Web Component — this package) │ │
|
|
32
|
+
│ │ Chat widget embedded into any web page │ │
|
|
33
|
+
│ └───────────────────┬──────────────────────────────┘ │
|
|
34
|
+
└───────────────────────│─────────────────────────────────┘
|
|
35
|
+
│ HTTPS (SSE stream + REST)
|
|
36
|
+
▼
|
|
37
|
+
┌───────────────────────────────────────────────────────────┐
|
|
38
|
+
│ Luma (Django / DRF backend) │
|
|
39
|
+
│ Agent config, auth, SSE proxy to Dana │
|
|
40
|
+
└───────────────────────┬───────────────────────────────────┘
|
|
41
|
+
│ HTTP (internal network)
|
|
42
|
+
▼
|
|
43
|
+
┌───────────────────────────────────────────────────────────┐
|
|
44
|
+
│ Dana (FastAPI / Python — AI backend) │
|
|
45
|
+
│ RAG pipeline, LLM integration, SSE streaming │
|
|
46
|
+
└───────────────────────────────────────────────────────────┘
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**OCWI** fetches its UI configuration (colors, agent name, language, …) from **Luma** at startup
|
|
50
|
+
and caches it in a cookie for instant subsequent loads. All chat messages are sent to **Luma**,
|
|
51
|
+
which proxies them to **Dana** and streams the AI response back over SSE.
|
|
52
|
+
Dana's URL is never exposed to the browser — Luma acts as the security boundary.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
### CDN (recommended for most use cases)
|
|
59
|
+
|
|
60
|
+
```html
|
|
61
|
+
<!-- jsDelivr (recommended) -->
|
|
62
|
+
<script src="https://cdn.jsdelivr.net/npm/ocwi/dist/ocwi.min.js"></script>
|
|
63
|
+
|
|
64
|
+
<!-- unpkg (alternative) -->
|
|
65
|
+
<script src="https://unpkg.com/ocwi/dist/ocwi.min.js"></script>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Both CDNs serve the `latest` tag by default. To pin to a specific version:
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<script src="https://cdn.jsdelivr.net/npm/ocwi@0.1.1/dist/ocwi.min.js"></script>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### npm
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm install ocwi
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Quick start
|
|
83
|
+
|
|
84
|
+
```html
|
|
85
|
+
<script src="https://cdn.jsdelivr.net/npm/ocwi/dist/ocwi.min.js"></script>
|
|
86
|
+
<div id="chat"></div>
|
|
87
|
+
<script>
|
|
88
|
+
const inst = window.OCWI('#chat', {
|
|
89
|
+
api: {
|
|
90
|
+
lumaUrl: 'https://your-luma-server.example.com/public/abc123'
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
</script>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Without a `lumaUrl`, the widget starts in **demo mode** using a built-in mock API:
|
|
97
|
+
|
|
98
|
+
```html
|
|
99
|
+
<script src="https://cdn.jsdelivr.net/npm/ocwi/dist/ocwi.min.js"></script>
|
|
100
|
+
<div id="chat"></div>
|
|
101
|
+
<script>
|
|
102
|
+
window.OCWI('#chat', {
|
|
103
|
+
ui: { name: 'Demo Assistant' },
|
|
104
|
+
theme: { primary: '#0ea5e9' },
|
|
105
|
+
locale: 'cs'
|
|
106
|
+
});
|
|
107
|
+
</script>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Public API
|
|
113
|
+
|
|
114
|
+
### `window.OCWI(target?, config?)`
|
|
115
|
+
|
|
116
|
+
Creates an `<ocwi-chat>` element, mounts it into the DOM and applies config.
|
|
117
|
+
|
|
118
|
+
| Parameter | Type | Description |
|
|
119
|
+
| --------- | --------------------------- | ------------------------------------------------------------------- |
|
|
120
|
+
| `target` | `string \| Element \| null` | CSS selector or Element to mount into. Defaults to `document.body`. |
|
|
121
|
+
| `config` | `Partial<OcwiConfig>` | Optional local config (merged last — highest priority). |
|
|
122
|
+
|
|
123
|
+
Returns the `<ocwi-chat>` element instance.
|
|
124
|
+
|
|
125
|
+
### Instance methods
|
|
126
|
+
|
|
127
|
+
| Method | Description |
|
|
128
|
+
| ----------------------- | ------------------------------------------------------------------------- |
|
|
129
|
+
| `updateConfig(partial)` | Deep-merge partial config; live update (no reload). |
|
|
130
|
+
| `getState()` | Returns current widget state `{ messages, isStreaming, windowState, … }`. |
|
|
131
|
+
| `destroy()` | Cleans up all listeners and removes the element from DOM. |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Configuration
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
window.OCWI('#chat', {
|
|
139
|
+
api: {
|
|
140
|
+
lumaUrl: 'https://luma.example.com/public/abc123', // Luma public link
|
|
141
|
+
danaUrl: 'https://dana.example.com', // Direct Dana URL (advanced, no proxy)
|
|
142
|
+
timeoutMs: 30000
|
|
143
|
+
},
|
|
144
|
+
locale: 'cs', // Top-level shortcut for i18n.lang
|
|
145
|
+
i18n: {
|
|
146
|
+
lang: 'cs',
|
|
147
|
+
dictionary: {
|
|
148
|
+
cs: { send: 'Odeslat' } // Override individual keys
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
theme: {
|
|
152
|
+
primary: '#0ea5e9',
|
|
153
|
+
ocwiWidth: '360px',
|
|
154
|
+
ocwiHeight: '520px',
|
|
155
|
+
ocwiRadius: '16px'
|
|
156
|
+
},
|
|
157
|
+
ui: {
|
|
158
|
+
name: 'Asistent',
|
|
159
|
+
placeholder: 'Napište zprávu…',
|
|
160
|
+
introductionMessage: 'Dobrý den! Jak vám mohu pomoci?',
|
|
161
|
+
position: 'bottom-right',
|
|
162
|
+
initialState: 'collapsed', // 'collapsed' | 'minimized' | 'expanded'
|
|
163
|
+
keybindForSend: 'Enter' // 'Enter' | 'Shift+Enter' | 'Ctrl+Enter' | 'Alt+Enter' | 'None'
|
|
164
|
+
},
|
|
165
|
+
features: {
|
|
166
|
+
minimize: true,
|
|
167
|
+
close: true,
|
|
168
|
+
serverStatus: true,
|
|
169
|
+
messageCopy: true,
|
|
170
|
+
messageEdit: true,
|
|
171
|
+
messageRefresh: true,
|
|
172
|
+
sendButton: true,
|
|
173
|
+
stopButton: true,
|
|
174
|
+
placeholder: true
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### `api`
|
|
180
|
+
|
|
181
|
+
| Field | Type | Description |
|
|
182
|
+
| ----------- | -------- | ------------------------------------------------------------------------------------------ |
|
|
183
|
+
| `lumaUrl` | `string` | Luma public link URL. Widget fetches UI config from here and routes all chat through Luma. |
|
|
184
|
+
| `danaUrl` | `string` | Direct Dana endpoint (bypasses Luma proxy — advanced use only). |
|
|
185
|
+
| `timeoutMs` | `number` | HTTP timeout in ms. Default: `30000`. |
|
|
186
|
+
|
|
187
|
+
### `theme`
|
|
188
|
+
|
|
189
|
+
| Field | CSS variable | Example |
|
|
190
|
+
| ------------------------- | ------------------------------ | ------------ |
|
|
191
|
+
| `primary` | `--ocwi-primary` | `'#0ea5e9'` |
|
|
192
|
+
| `ocwiWidth` | `--ocwi-width` | `'360px'` |
|
|
193
|
+
| `ocwiHeight` | `--ocwi-height` | `'520px'` |
|
|
194
|
+
| `ocwiRadius` | `--ocwi-radius` | `'12px'` |
|
|
195
|
+
| `ocwiSpacing` | `--ocwi-spacing` | `'8px'` |
|
|
196
|
+
| `ocwiBg` | `--ocwi-bg` | `'#ffffff'` |
|
|
197
|
+
| `ocwiHeaderBg` | `--ocwi-header-bg` | `'#f9fafb'` |
|
|
198
|
+
| `ocwiHeaderText` | `--ocwi-header-text` | `'#111111'` |
|
|
199
|
+
| `ocwiBubbleUserBg` | `--ocwi-bubble-user-bg` | `'#2563eb'` |
|
|
200
|
+
| `ocwiBubbleUserText` | `--ocwi-bubble-user-text` | `'#ffffff'` |
|
|
201
|
+
| `ocwiBubbleAssistantBg` | `--ocwi-bubble-assistant-bg` | `'#f3f4f6'` |
|
|
202
|
+
| `ocwiBubbleAssistantText` | `--ocwi-bubble-assistant-text` | `'#1f2937'` |
|
|
203
|
+
| `ocwiInputBg` | `--ocwi-input-bg` | `'#ffffff'` |
|
|
204
|
+
| `ocwiSendBg` | `--ocwi-send-bg` | `'#2563eb'` |
|
|
205
|
+
| `ocwiFabBg` | `--ocwi-fab-bg` | `'#2563eb'` |
|
|
206
|
+
| `ocwiZIndex` | `--ocwi-z-index` | `2147480000` |
|
|
207
|
+
| `ocwiInputRows` | _(textarea rows)_ | `1` |
|
|
208
|
+
|
|
209
|
+
CSS variables can also be set directly on the host element from the outside:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
document.querySelector('ocwi-chat').style.setProperty('--ocwi-primary', '#8b5cf6');
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## HTTP (Dana) API contract
|
|
216
|
+
|
|
217
|
+
When `danaUrl` is configured, the widget POSTs to `<danaUrl>/api/chat`:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
POST https://dana.example.com/api/chat
|
|
221
|
+
Content-Type: application/json
|
|
222
|
+
|
|
223
|
+
{
|
|
224
|
+
"prompt": "Hello",
|
|
225
|
+
"conversationId": "c_01j…",
|
|
226
|
+
"clientMessageId": "u_01j…",
|
|
227
|
+
"rewriteFromMessageId": "u_01j…" // only on edit / refresh
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The response is an SSE stream. The widget reads chunks and accumulates them as the assistant message text. Dana signals errors via structured SSE events:
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
data: {"error": "DANA_LLM_UNAVAILABLE"}\n\n
|
|
235
|
+
data: {"error": "DANA_LLM_AUTH"}\n\n
|
|
236
|
+
data: {"error": "DANA_LLM_TIMEOUT"}\n\n
|
|
237
|
+
data: {"error": "DANA_INTERNAL"}\n\n
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
OCWI detects these events, aborts the stream, and displays a localised error message in the chat.
|
|
241
|
+
|
|
242
|
+
**In production, all traffic goes through Luma** — the `danaUrl` field is for advanced / self-hosted setups where you contact Dana directly.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## i18n
|
|
247
|
+
|
|
248
|
+
Built-in languages: `en` (default), `cs`, `de`.
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
window.OCWI('#chat', {
|
|
252
|
+
locale: 'cs',
|
|
253
|
+
i18n: {
|
|
254
|
+
dictionary: {
|
|
255
|
+
cs: { send: 'Pošli' } // override individual keys
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
To add a new language entirely:
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
window.OCWI('#chat', {
|
|
265
|
+
locale: 'sk',
|
|
266
|
+
i18n: {
|
|
267
|
+
lang: 'sk',
|
|
268
|
+
dictionary: {
|
|
269
|
+
sk: {
|
|
270
|
+
send: 'Odoslať',
|
|
271
|
+
stop: 'Zastaviť'
|
|
272
|
+
// … add all keys (falls back to 'en' for missing ones)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Accessibility
|
|
282
|
+
|
|
283
|
+
- Widget container has `role="region"` and `aria-label` with the agent name
|
|
284
|
+
- Textarea has `aria-label` (uses placeholder text)
|
|
285
|
+
- All icon buttons have `aria-label`
|
|
286
|
+
- Send button is focusable and keyboard-operable
|
|
287
|
+
- `Enter` submits (configurable), `Escape` clears the input
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Development
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
npm install # install deps
|
|
295
|
+
npm run build # → dist/ocwi.min.js
|
|
296
|
+
npm run dev # watch mode
|
|
297
|
+
npm run lint # ESLint
|
|
298
|
+
npm run format # Prettier
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
OCWI is distributed under the [OCWI Sustainable Use License](LICENSE.md).
|
|
306
|
+
Source available. Free for personal, non-commercial, and internal business use.
|
|
307
|
+
Commercial redistribution or selling the widget as a standalone product requires a separate agreement.
|