surf-core 0.1.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/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/plugins/debounce.js +2 -0
- package/dist/plugins/drag-and-drop.js +2 -0
- package/dist/surf.js +911 -0
- package/dist/surf.min.js +5 -0
- package/package.json +46 -0
- package/src/cell.js +197 -0
- package/src/echo.js +63 -0
- package/src/patch.js +81 -0
- package/src/plugins/auto-refresh.js +70 -0
- package/src/plugins/debounce.js +49 -0
- package/src/plugins/drag-and-drop.js +159 -0
- package/src/pulse.js +369 -0
- package/src/signal.js +507 -0
- package/src/surf.d.ts +77 -0
- package/src/surf.js +179 -0
- package/src/surface.js +160 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Berkan Çetinkaya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# SURF [](https://ci.berkan.cc/repos/2)
|
|
2
|
+
|
|
3
|
+
HTML-first, server-driven UI framework with local client-side state.
|
|
4
|
+
|
|
5
|
+
> **Mental Model**: "Surface changes, Cell lives."
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Server is the source of truth** — All data and validation lives on the server
|
|
10
|
+
- **Client handles local motion** — Only temporary, interactive state on the client
|
|
11
|
+
- **HTML is the data format** — UI changes through HTML patches, not JSON APIs
|
|
12
|
+
- **No build step required** — Works directly in the browser, no transpilation needed
|
|
13
|
+
- **Progressive enhancement** — Works without JS, enhanced with JS
|
|
14
|
+
|
|
15
|
+
## Non-Goals
|
|
16
|
+
|
|
17
|
+
- ❌ Global client-side store
|
|
18
|
+
- ❌ Virtual DOM
|
|
19
|
+
- ❌ Mandatory JSON APIs
|
|
20
|
+
- ❌ Hidden magic or implicit behavior
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<script src="https://unpkg.com/surf@latest/dist/surf.min.js"></script>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install via npm:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install surf
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Core Concepts
|
|
35
|
+
|
|
36
|
+
### Surface
|
|
37
|
+
|
|
38
|
+
A Surface is a DOM region that can be replaced by server responses.
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<main id="main" d-surface>
|
|
42
|
+
<!-- Content that can be replaced -->
|
|
43
|
+
</main>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Cell
|
|
47
|
+
|
|
48
|
+
A Cell is a local, client-side state container. Cells survive Surface updates.
|
|
49
|
+
|
|
50
|
+
```html
|
|
51
|
+
<div d-cell="{ count: 0 }">
|
|
52
|
+
<span d-text="count"></span>
|
|
53
|
+
<button d-signal="click: count = count + 1">+</button>
|
|
54
|
+
</div>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Signal
|
|
58
|
+
|
|
59
|
+
Signals define reactive behavior inside a Cell.
|
|
60
|
+
|
|
61
|
+
| `d-signal` | Event handler | `d-signal="click: open = true"` |
|
|
62
|
+
| `d-text` | Text binding | `d-text="count"` |
|
|
63
|
+
| `d-show` | Conditional display | `d-show="open"` |
|
|
64
|
+
| `d-attr` | Attribute binding | `d-attr="disabled: loading"` |
|
|
65
|
+
| `d-attr` | Class toggling | `d-attr="class.active: isActive"` |
|
|
66
|
+
|
|
67
|
+
### Signal Features
|
|
68
|
+
|
|
69
|
+
**Form Resetting:**
|
|
70
|
+
Use the `reset` keyword to clear forms declaratively. Surf automatically ensures this happens *after* submission data is captured.
|
|
71
|
+
|
|
72
|
+
```html
|
|
73
|
+
<form d-pulse="commit" d-signal="submit: reset">
|
|
74
|
+
<input name="msg">
|
|
75
|
+
</form>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Native Methods (`this.method()`):**
|
|
79
|
+
Call native DOM methods on the element triggering the signal.
|
|
80
|
+
|
|
81
|
+
```html
|
|
82
|
+
<input type="text" d-signal="focus: this.select()">
|
|
83
|
+
<video d-signal="mouseenter: this.play()"></video>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Plugins
|
|
87
|
+
|
|
88
|
+
Surf supports a lightweight plugin system.
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
import Surf from './surf.js';
|
|
92
|
+
import DragAndDrop from './plugins/drag-and-drop.js';
|
|
93
|
+
|
|
94
|
+
Surf.use(DragAndDrop);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Drag & Drop Plugin:**
|
|
98
|
+
Enable drag-and-drop reordering with simple attributes.
|
|
99
|
+
|
|
100
|
+
```html
|
|
101
|
+
<div d-drag-zone="group-name" d-drag-url="/api/move">
|
|
102
|
+
<div d-drag-handle>...</div>
|
|
103
|
+
</div>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Pulse
|
|
107
|
+
|
|
108
|
+
A Pulse triggers server interaction.
|
|
109
|
+
|
|
110
|
+
```html
|
|
111
|
+
<!-- Navigation (GET) -->
|
|
112
|
+
<a href="/page" d-pulse="navigate" d-target="#main">Go to Page</a>
|
|
113
|
+
|
|
114
|
+
<!-- Action (POST) -->
|
|
115
|
+
<!-- Sends Cell state + data attributes -->
|
|
116
|
+
<button d-pulse="action" d-action="/api/like" data-id="123">Like</button>
|
|
117
|
+
|
|
118
|
+
<!-- Form submission (POST) -->
|
|
119
|
+
<form d-pulse="commit" d-target="#form">
|
|
120
|
+
<input name="email" required>
|
|
121
|
+
<button type="submit">Submit</button>
|
|
122
|
+
</form>
|
|
123
|
+
|
|
124
|
+
<!-- Refresh content -->
|
|
125
|
+
<button d-pulse="refresh" d-target="#main">Refresh</button>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Auto-Refresh
|
|
129
|
+
|
|
130
|
+
Surfaces can automatically poll the server for updates.
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
<div d-surface d-auto-refresh="5000" d-auto-refresh-url="/api/news">
|
|
134
|
+
<!-- Content updates every 5 seconds -->
|
|
135
|
+
</div>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Patch
|
|
139
|
+
|
|
140
|
+
Server returns HTML patches to update Surfaces.
|
|
141
|
+
|
|
142
|
+
```html
|
|
143
|
+
<d-patch>
|
|
144
|
+
<surface target="#main">
|
|
145
|
+
<h1>Updated Content</h1>
|
|
146
|
+
</surface>
|
|
147
|
+
<surface target="#toast">
|
|
148
|
+
<div class="toast">Saved!</div>
|
|
149
|
+
</surface>
|
|
150
|
+
</d-patch>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## JavaScript API
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
// Navigate to URL
|
|
157
|
+
Surf.go('/page', { target: '#main' });
|
|
158
|
+
|
|
159
|
+
// Refresh a surface
|
|
160
|
+
Surf.refresh('#main');
|
|
161
|
+
|
|
162
|
+
// Listen to events
|
|
163
|
+
Surf.on('before:pulse', (e) => console.log('Loading...'));
|
|
164
|
+
Surf.on('after:patch', (e) => console.log('Done!'));
|
|
165
|
+
Surf.on('error:network', (e) => console.error(e.error));
|
|
166
|
+
|
|
167
|
+
// Manual state access
|
|
168
|
+
Surf.getState('#my-cell');
|
|
169
|
+
Surf.setState('#my-cell', { count: 5 });
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Echo Rule
|
|
173
|
+
|
|
174
|
+
> "Surface changes, Cell lives."
|
|
175
|
+
|
|
176
|
+
When a Surface is patched, Cell states are preserved. If a Cell with the same `id` or `d-cell-id` exists in the new content, its state is restored automatically.
|
|
177
|
+
|
|
178
|
+
```html
|
|
179
|
+
<!-- Before patch: count = 5 -->
|
|
180
|
+
<div id="counter" d-cell="{ count: 0 }">
|
|
181
|
+
<span d-text="count">5</span>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<!-- After patch: count still = 5 (preserved by Echo) -->
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Install dependencies
|
|
191
|
+
npm install
|
|
192
|
+
|
|
193
|
+
# Build
|
|
194
|
+
npm run build
|
|
195
|
+
|
|
196
|
+
# Watch mode
|
|
197
|
+
npm run dev
|
|
198
|
+
|
|
199
|
+
# Serve examples
|
|
200
|
+
npm run serve
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
MIT
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var SurfDebounce=(()=>{var i=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var b=Object.getOwnPropertyNames;var h=Object.prototype.hasOwnProperty;var f=(n,t)=>{for(var o in t)i(n,o,{get:t[o],enumerable:!0})},A=(n,t,o,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of b(t))!h.call(n,r)&&r!==o&&i(n,r,{get:()=>t[r],enumerable:!(e=p(t,r))||e.enumerable});return n};var $=n=>A(i({},"__esModule",{value:!0}),n);var I={};f(I,{default:()=>v});var v={install(n){let t=new WeakMap;document.addEventListener("input",o=>{let e=o.target;if(!e.hasAttribute("d-input"))return;let r=e.getAttribute("d-input"),u=parseInt(e.getAttribute("d-debounce")||"300",10),a=e.getAttribute("d-target"),c=parseInt(e.getAttribute("d-min-length")||"3",10),s=e.value.trim();t.has(e)&&clearTimeout(t.get(e));let l=setTimeout(()=>{if(s.length>0&&s.length<c)return;let g=r.includes("?")?"&":"?",d=e.name||"q",m=`${r}${g}${d}=${encodeURIComponent(s)}`;n.go(m,{target:a})},u);t.set(e,l)}),console.log("[Surf] Debounce Plugin installed")}};return $(I);})();
|
|
2
|
+
window.SurfDebounce=SurfDebounce.default
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var SurfDragAndDrop=(()=>{var u=Object.defineProperty;var b=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var D=Object.prototype.hasOwnProperty;var h=(a,o)=>{for(var n in o)u(a,n,{get:o[n],enumerable:!0})},L=(a,o,n,c)=>{if(o&&typeof o=="object"||typeof o=="function")for(let s of m(o))!D.call(a,s)&&s!==n&&u(a,s,{get:()=>o[s],enumerable:!(c=b(o,s))||c.enumerable});return a};var y=a=>L(u({},"__esModule",{value:!0}),a);var E={};h(E,{default:()=>A});var w={name:"drag-and-drop",install(a,o={}){let n=null,c=(t=document)=>{t.querySelectorAll('[d-draggable="true"]').forEach(e=>{e.setAttribute("draggable","true")})};c(),new MutationObserver(t=>{t.forEach(e=>{e.addedNodes.forEach(r=>{r.nodeType===1&&(r.hasAttribute("d-draggable")&&r.setAttribute("draggable","true"),c(r))})})}).observe(document.body,{childList:!0,subtree:!0}),document.addEventListener("dragstart",t=>{if(t.target.closest("[d-no-drag]")){t.preventDefault();return}if(!t.target.closest('[d-draggable="true"]'))return;let e=t.target.closest('[d-draggable="true"]');n=e;let r=e.getAttribute("d-drag-data");r&&t.dataTransfer.setData("text/plain",r),t.dataTransfer.effectAllowed="move",e.classList.add("dragging")}),document.addEventListener("dragend",t=>{n&&(n.classList.remove("dragging"),n=null),document.querySelectorAll(".drag-over").forEach(e=>{e.classList.remove("drag-over")})}),document.addEventListener("dragover",t=>{let e=t.target.closest("[d-drop-zone]");e&&(t.preventDefault(),t.dataTransfer.dropEffect="move",e.classList.add("drag-over"))}),document.addEventListener("dragleave",t=>{let e=t.target.closest("[d-drop-zone]");if(!e)return;let r=e.getBoundingClientRect(),d=t.clientX,l=t.clientY;(d<r.left||d>=r.right||l<r.top||l>=r.bottom)&&e.classList.remove("drag-over")}),document.addEventListener("drop",async t=>{let e=t.target.closest("[d-drop-zone]");if(!e)return;t.preventDefault(),e.classList.remove("drag-over");let r=e.getAttribute("d-drop-url");if(r)try{let d=t.dataTransfer.getData("text/plain");if(!d)return;let l=JSON.parse(d),f=e.getAttribute("d-drop-data");if(f){let i=JSON.parse(f);Object.assign(l,i)}let p=new URLSearchParams;for(let[i,v]of Object.entries(l))p.append(i,v);let g=await fetch(r,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:p});if(g.ok){let i=await g.text();a&&a.applyPatch&&a.applyPatch(i)}else console.error("[Surf DnD] Drop request failed",g.status)}catch(d){console.error("[Surf DnD] Drop error",d)}}),console.log("[Surf] Drag & Drop plugin installed")}},A=w;return y(E);})();
|
|
2
|
+
window.SurfDragAndDrop=SurfDragAndDrop.default
|