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/src/surf.js ADDED
@@ -0,0 +1,179 @@
1
+ /**
2
+ * SURF - HTML-first, server-driven UI framework
3
+ *
4
+ * Mental model: "Surface changes, Cell lives."
5
+ *
6
+ * The server is the source of truth.
7
+ * The client handles only temporary, local interactions.
8
+ * HTML is the primary data format.
9
+ * UI changes happen through HTML patches, not JSON APIs.
10
+ */
11
+
12
+ import * as Surface from './surface.js';
13
+ import * as Cell from './cell.js';
14
+ import * as Signal from './signal.js';
15
+ import * as Pulse from './pulse.js';
16
+ import * as Patch from './patch.js';
17
+ import * as Echo from './echo.js';
18
+
19
+ /**
20
+ * Global Surf object - the public API
21
+ */
22
+ const Surf = {
23
+ /**
24
+ * Framework version
25
+ */
26
+ version: '0.1.0',
27
+
28
+ /**
29
+ * Navigate to a URL
30
+ * @param {string} url - The URL to navigate to
31
+ * @param {Object} options - Optional settings (target, swap, etc)
32
+ */
33
+ go(url, options = {}) {
34
+ return Pulse.go(url, options);
35
+ },
36
+
37
+ /**
38
+ * Refresh a surface's content from the server
39
+ * @param {string} selector - The surface selector to refresh
40
+ */
41
+ refresh(selector) {
42
+ return Pulse.refresh(selector);
43
+ },
44
+
45
+ /**
46
+ * Subscribe to framework events
47
+ * @param {string} event - Event name: 'before:pulse', 'after:patch', 'error:network'
48
+ * @param {function} callback - Event handler
49
+ */
50
+ on(event, callback) {
51
+ Pulse.on(event, callback);
52
+ },
53
+
54
+ /**
55
+ * Unsubscribe from framework events
56
+ * @param {string} event - Event name
57
+ * @param {function} callback - Event handler to remove
58
+ */
59
+ off(event, callback) {
60
+ Pulse.off(event, callback);
61
+ },
62
+
63
+ /**
64
+ * Get cell state for an element
65
+ * @param {Element|string} cellOrSelector - Cell element or selector
66
+ * @returns {Object} The cell's current state
67
+ */
68
+ getState(cellOrSelector) {
69
+ const cell = typeof cellOrSelector === 'string'
70
+ ? document.querySelector(cellOrSelector)
71
+ : cellOrSelector;
72
+ return Cell.getState(cell);
73
+ },
74
+
75
+ /**
76
+ * Set cell state for an element
77
+ * @param {Element|string} cellOrSelector - Cell element or selector
78
+ * @param {Object} state - State to merge
79
+ */
80
+ setState(cellOrSelector, state) {
81
+ const cell = typeof cellOrSelector === 'string'
82
+ ? document.querySelector(cellOrSelector)
83
+ : cellOrSelector;
84
+ Cell.setState(cell, state);
85
+ Signal.updateBindings(cell);
86
+ },
87
+
88
+ /**
89
+ * Manually apply a patch response
90
+ * @param {string} patchHtml - The patch HTML string
91
+ */
92
+ applyPatch(patchHtml) {
93
+ const patches = Patch.parse(patchHtml);
94
+ patches.forEach(({ target, content }) => {
95
+ const surface = Surface.getBySelector(target) || document.querySelector(target);
96
+ if (surface) {
97
+ Echo.withPreservation(surface, content, () => {
98
+ Surface.replace(target, content);
99
+ // Re-initialize signals and cells on the updated surface
100
+ Cell.initAll(surface);
101
+ Signal.initAll(surface);
102
+ });
103
+ }
104
+ });
105
+ },
106
+
107
+ /**
108
+ * Register a module for signal expressions
109
+ * @param {string} name - Module namespace
110
+ * @param {Object} module - Object with methods
111
+ */
112
+ register(name, module) {
113
+ Signal.register(name, module);
114
+ },
115
+
116
+ /**
117
+ * Install a plugin
118
+ * @param {Object} plugin - Plugin object with install method
119
+ * @param {Object} options - Plugin options
120
+ */
121
+ use(plugin, options = {}) {
122
+ if (plugin && typeof plugin.install === 'function') {
123
+ plugin.install(this, options);
124
+ }
125
+ return this;
126
+ },
127
+
128
+
129
+
130
+
131
+
132
+ // Expose modules for advanced usage
133
+ _modules: {
134
+ Surface,
135
+ Cell,
136
+ Signal,
137
+ Pulse,
138
+ Patch,
139
+ Echo
140
+ }
141
+ };
142
+
143
+ /**
144
+ * Initialize SURF when the DOM is ready
145
+ */
146
+ function init() {
147
+ // Initialize surfaces
148
+ Surface.init();
149
+
150
+ // Initialize cells
151
+ Cell.initAll();
152
+
153
+ // Initialize signals (reactive bindings)
154
+ Signal.initAll();
155
+
156
+ // Initialize pulse (event interception)
157
+ Pulse.init();
158
+
159
+ // Register core modules for signals
160
+ Signal.register('Pulse', Pulse);
161
+
162
+ console.log(`[Surf] Initialized v${Surf.version}`);
163
+ }
164
+
165
+ // Auto-initialize on DOMContentLoaded
166
+ if (document.readyState === 'loading') {
167
+ document.addEventListener('DOMContentLoaded', init);
168
+ } else {
169
+ init();
170
+ }
171
+
172
+ // Export for module usage
173
+ export { Surface, Cell, Signal, Pulse, Patch, Echo };
174
+ export default Surf;
175
+
176
+ // Attach to window for script tag usage
177
+ if (typeof window !== 'undefined') {
178
+ window.Surf = Surf;
179
+ }
package/src/surface.js ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Surface Module
3
+ *
4
+ * A Surface is a DOM region that can be replaced by server responses.
5
+ * Defined with: d-surface
6
+ */
7
+
8
+ const SURFACE_ATTR = 'd-surface';
9
+
10
+ /**
11
+ * Find all surface elements in the document
12
+ * @returns {NodeListOf<Element>}
13
+ */
14
+ export function findAll() {
15
+ return document.querySelectorAll(`[${SURFACE_ATTR}]`);
16
+ }
17
+
18
+ /**
19
+ * Get a surface element by its ID
20
+ * @param {string} id - The surface ID (without #)
21
+ * @returns {Element|null}
22
+ */
23
+ export function getById(id) {
24
+ const cleanId = id.startsWith('#') ? id.slice(1) : id;
25
+ const element = document.getElementById(cleanId);
26
+
27
+ if (element && element.hasAttribute(SURFACE_ATTR)) {
28
+ return element;
29
+ }
30
+
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * Get a surface element by selector
36
+ * @param {string} selector - CSS selector
37
+ * @returns {Element|null}
38
+ */
39
+ export function getBySelector(selector) {
40
+ const element = document.querySelector(selector);
41
+
42
+ if (element && element.hasAttribute(SURFACE_ATTR)) {
43
+ return element;
44
+ }
45
+
46
+ return null;
47
+ }
48
+
49
+ /**
50
+ * Replace surface content with new HTML
51
+ * @param {string|Element} selectorOrElement - Target surface selector or element
52
+ * @param {string} html - New HTML content
53
+ * @returns {Element|null} - The updated surface element
54
+ */
55
+ export function replace(selectorOrElement, html) {
56
+ // Handle both selector strings and element references
57
+ let surface;
58
+ if (typeof selectorOrElement === 'string') {
59
+ surface = getBySelector(selectorOrElement) || document.querySelector(selectorOrElement);
60
+ } else {
61
+ surface = selectorOrElement;
62
+ }
63
+
64
+ if (!surface) {
65
+ console.warn(`[Surf] Surface not found: ${selectorOrElement}`);
66
+ return null;
67
+ }
68
+
69
+ // Create a template to parse the HTML
70
+ const template = document.createElement('template');
71
+ template.innerHTML = html.trim();
72
+
73
+ // Replace inner content, preserving the surface element itself
74
+ surface.innerHTML = '';
75
+
76
+ // Append all children from the template
77
+ while (template.content.firstChild) {
78
+ surface.appendChild(template.content.firstChild);
79
+ }
80
+
81
+ return surface;
82
+ }
83
+
84
+ /**
85
+ * Append content to surface
86
+ * @param {string|Element} selectorOrElement
87
+ * @param {string} html
88
+ * @returns {Element|null}
89
+ */
90
+ export function append(selectorOrElement, html) {
91
+ return inject(selectorOrElement, html, 'beforeend');
92
+ }
93
+
94
+ /**
95
+ * Prepend content to surface
96
+ * @param {string|Element} selectorOrElement
97
+ * @param {string} html
98
+ * @returns {Element|null}
99
+ */
100
+ export function prepend(selectorOrElement, html) {
101
+ return inject(selectorOrElement, html, 'afterbegin');
102
+ }
103
+
104
+ /**
105
+ * Inject content into surface at position
106
+ * @param {string|Element} selectorOrElement
107
+ * @param {string} html
108
+ * @param {string} position - 'beforeend' or 'afterbegin'
109
+ * @returns {Element|null}
110
+ */
111
+ function inject(selectorOrElement, html, position) {
112
+ let surface;
113
+ if (typeof selectorOrElement === 'string') {
114
+ surface = getBySelector(selectorOrElement) || document.querySelector(selectorOrElement);
115
+ } else {
116
+ surface = selectorOrElement;
117
+ }
118
+
119
+ if (!surface) {
120
+ console.warn(`[Surf] Surface not found: ${selectorOrElement}`);
121
+ return null;
122
+ }
123
+
124
+ const template = document.createElement('template');
125
+ template.innerHTML = html.trim();
126
+
127
+ // Use DocumentFragment for efficient insertion
128
+ const fragment = document.createDocumentFragment();
129
+ while (template.content.firstChild) {
130
+ fragment.appendChild(template.content.firstChild);
131
+ }
132
+
133
+ if (position === 'beforeend') {
134
+ surface.appendChild(fragment);
135
+ } else {
136
+ surface.insertBefore(fragment, surface.firstChild);
137
+ }
138
+
139
+ return surface;
140
+ }
141
+
142
+ /**
143
+ * Initialize surfaces - mark them as ready
144
+ */
145
+ export function init() {
146
+ const surfaces = findAll();
147
+ surfaces.forEach(surface => {
148
+ surface.setAttribute('data-surf-ready', 'true');
149
+ });
150
+ }
151
+
152
+ export default {
153
+ findAll,
154
+ getById,
155
+ getBySelector,
156
+ replace,
157
+ append,
158
+ prepend,
159
+ init
160
+ };