Skip to content

Commit 1201e11

Browse files
committed
chore: wip
1 parent 6af36e5 commit 1201e11

9 files changed

Lines changed: 973 additions & 32 deletions

File tree

examples/native-tahoe-sidebar.stx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Native Tahoe Sidebar</title>
7+
</head>
8+
<body class="m-0 min-h-screen overflow-hidden bg-zinc-100 text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50">
9+
@craft
10+
11+
<main class="flex h-screen">
12+
<@craft-sidebar
13+
id="tahoe-sidebar"
14+
class="m-3"
15+
selectedItem="inbox"
16+
searchPlaceholder="Search"
17+
variant="desktop"
18+
material="sidebar"
19+
backgroundEffect="shimmer"
20+
allowsVibrancy="true"
21+
materialOpacity="0.62"
22+
minWidth="240"
23+
maxWidth="340"
24+
>
25+
<@craft-sidebar-section id="favorites" title="Favorites">
26+
<@craft-sidebar-item id="inbox" label="Inbox" icon="tray.fill" badge="12" />
27+
<@craft-sidebar-item id="today" label="Today" icon="calendar" />
28+
<@craft-sidebar-item id="starred" label="Starred" icon="star.fill" />
29+
</@craft-sidebar-section>
30+
31+
<@craft-sidebar-section id="folders" title="Folders">
32+
<@craft-sidebar-item id="documents" label="Documents" icon="doc.text" />
33+
<@craft-sidebar-item id="projects" label="Projects" icon="folder.badge.gearshape" />
34+
<@craft-sidebar-item id="archive" label="Archive" icon="archivebox.fill" />
35+
</@craft-sidebar-section>
36+
37+
<@craft-sidebar-section id="tags" title="Tags">
38+
<@craft-sidebar-item id="important" label="Important" icon="circle.fill" tintColor="#ef4444" />
39+
<@craft-sidebar-item id="work" label="Work" icon="circle.fill" tintColor="#0a84ff" />
40+
<@craft-sidebar-item id="personal" label="Personal" icon="circle.fill" tintColor="#34c759" />
41+
</@craft-sidebar-section>
42+
</@craft-sidebar>
43+
44+
<section class="flex min-w-0 flex-1 flex-col">
45+
<header class="flex h-14 items-center border-b border-black/10 px-6 dark:border-white/10">
46+
<h1 class="text-base font-semibold">Inbox</h1>
47+
</header>
48+
49+
<div class="grid flex-1 place-items-center p-8">
50+
<div class="max-w-lg rounded-lg border border-black/10 bg-white/75 p-6 shadow-sm dark:border-white/10 dark:bg-white/5">
51+
<h2 class="mb-2 text-lg font-semibold">Native macOS sidebar</h2>
52+
<p class="text-sm leading-6 text-zinc-600 dark:text-zinc-300">
53+
In Craft on macOS, this STX-authored sidebar is promoted to the native Tahoe-style
54+
sidebar with AppKit material. In a browser, the same markup keeps a glass fallback.
55+
</p>
56+
</div>
57+
</div>
58+
</section>
59+
</main>
60+
</body>
61+
</html>

packages/stx/src/craft-bridge.ts

Lines changed: 126 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,130 @@ else if (data.type === 'event' && data.method) {
178178
};
179179
}
180180
181+
function sendNativeUI(action, data) {
182+
if (window.webkit?.messageHandlers?.craft) {
183+
window.webkit.messageHandlers.craft.postMessage({ t: 'nativeUI', a: action, d: data || {} });
184+
return true;
185+
}
186+
187+
if (window.CraftBridge?.postMessage) {
188+
window.CraftBridge.postMessage(JSON.stringify({ t: 'nativeUI', a: action, d: data || {} }));
189+
return true;
190+
}
191+
192+
return send({
193+
id: generateId(),
194+
type: 'nativeUI',
195+
action,
196+
data: data || {}
197+
});
198+
}
199+
200+
function createNativeUIFacade() {
201+
class Sidebar {
202+
constructor(id) {
203+
this.id = id;
204+
this._selectCallbacks = [];
205+
}
206+
addSection(section) {
207+
sendNativeUI('addSidebarSection', { sidebarId: this.id, section });
208+
return this;
209+
}
210+
setSelectedItem(itemId) {
211+
sendNativeUI('setSelectedItem', { sidebarId: this.id, itemId });
212+
return this;
213+
}
214+
onSelect(callback) {
215+
this._selectCallbacks.push(callback);
216+
return this;
217+
}
218+
destroy() {
219+
sendNativeUI('destroyComponent', { id: this.id, type: 'sidebar' });
220+
}
221+
}
222+
223+
class FileBrowser {
224+
constructor(id) {
225+
this.id = id;
226+
}
227+
addFile(file) {
228+
sendNativeUI('addFile', { browserId: this.id, file });
229+
return this;
230+
}
231+
addFiles(files) {
232+
sendNativeUI('addFiles', { browserId: this.id, files });
233+
return this;
234+
}
235+
clearFiles() {
236+
sendNativeUI('clearFiles', { browserId: this.id });
237+
return this;
238+
}
239+
destroy() {
240+
sendNativeUI('destroyComponent', { id: this.id, type: 'fileBrowser' });
241+
}
242+
}
243+
244+
class SplitView {
245+
constructor(id, sidebar, browser) {
246+
this.id = id;
247+
this.sidebar = sidebar;
248+
this.browser = browser;
249+
}
250+
setDividerPosition(position) {
251+
sendNativeUI('setDividerPosition', { splitViewId: this.id, position });
252+
return this;
253+
}
254+
destroy() {
255+
sendNativeUI('destroyComponent', { id: this.id, type: 'splitView' });
256+
}
257+
}
258+
259+
return {
260+
createSidebar(options) {
261+
const opts = options || {};
262+
const id = opts.id || 'sidebar-' + Date.now() + '-' + Math.random().toString(36).slice(2);
263+
sendNativeUI('createSidebar', Object.assign({}, opts, { id }));
264+
return new Sidebar(id);
265+
},
266+
createFileBrowser(options) {
267+
const opts = options || {};
268+
const id = opts.id || 'browser-' + Date.now() + '-' + Math.random().toString(36).slice(2);
269+
sendNativeUI('createFileBrowser', Object.assign({}, opts, { id }));
270+
return new FileBrowser(id);
271+
},
272+
createSplitView(options) {
273+
if (!options?.sidebar || !options?.browser) {
274+
throw new Error('createSplitView requires sidebar and browser options');
275+
}
276+
const id = options.id || 'splitview-' + Date.now() + '-' + Math.random().toString(36).slice(2);
277+
sendNativeUI('createSplitView', {
278+
id,
279+
sidebarId: options.sidebar.id,
280+
browserId: options.browser.id
281+
});
282+
return new SplitView(id, options.sidebar, options.browser);
283+
},
284+
showQuickLook(options) {
285+
sendNativeUI('showQuickLook', options);
286+
},
287+
closeQuickLook() {
288+
sendNativeUI('closeQuickLook', {});
289+
},
290+
toggleQuickLook(options) {
291+
sendNativeUI('toggleQuickLook', options);
292+
}
293+
};
294+
}
295+
296+
const existingCraft = window.craft || {};
297+
const nativeUI = existingCraft.nativeUI || createNativeUIFacade();
298+
const componentAPI = existingCraft.components || Object.assign({}, nativeUI, {
299+
updateComponent: (id, props) => request('component.update', { componentId: id, props }),
300+
destroyComponent: (id) => request('component.destroy', { componentId: id })
301+
});
302+
181303
// Build the craft API object
182-
window.craft = {
304+
window.craft = Object.assign({}, existingCraft, {
183305
// Meta
184306
isCraft,
185307
on,
@@ -289,14 +411,9 @@ else if (data.type === 'event' && data.method) {
289411
},
290412
291413
// Native component helpers
292-
components: {
293-
createSidebar: (config) => request('component.createSidebar', config),
294-
createFileBrowser: (config) => request('component.createFileBrowser', config),
295-
createSplitView: (config) => request('component.createSplitView', config),
296-
updateComponent: (id, props) => request('component.update', { componentId: id, props }),
297-
destroyComponent: (id) => request('component.destroy', { componentId: id }),
298-
}
299-
};
414+
nativeUI,
415+
components: componentAPI
416+
});
300417
301418
// Dispatch ready event
302419
window.dispatchEvent(new CustomEvent('craft:ready', { detail: { isCraft: isCraft() } }));

packages/stx/src/craft-compiler.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,52 @@ const NATIVE_COMPONENTS: Map<string, NativeComponentDefinition> = new Map([
409409
],
410410
children: 'multiple',
411411
}],
412+
['craft-sidebar', {
413+
name: 'Sidebar',
414+
zigType: 'craft.NativeSidebar',
415+
props: [
416+
{ name: 'id', type: 'string', required: false },
417+
{ name: 'variant', type: 'string', required: false, default: 'desktop' },
418+
{ name: 'material', type: 'string', required: false, default: 'sidebar' },
419+
{ name: 'backgroundEffect', type: 'string', required: false, default: 'shimmer' },
420+
{ name: 'allowsVibrancy', type: 'boolean', required: false, default: true },
421+
{ name: 'materialOpacity', type: 'number', required: false, default: 0.68 },
422+
{ name: 'minWidth', type: 'number', required: false, default: 240 },
423+
{ name: 'maxWidth', type: 'number', required: false, default: 340 },
424+
{ name: 'canCollapse', type: 'boolean', required: false, default: true },
425+
{ name: 'selectedItem', type: 'string', required: false },
426+
{ name: 'sections', type: 'array', required: false },
427+
],
428+
children: 'multiple',
429+
platform: ['macos'],
430+
fallback: 'aside',
431+
}],
432+
['craft-sidebar-section', {
433+
name: 'SidebarSection',
434+
zigType: 'craft.NativeSidebarSection',
435+
props: [
436+
{ name: 'id', type: 'string', required: true },
437+
{ name: 'title', type: 'string', required: false },
438+
{ name: 'header', type: 'string', required: false },
439+
],
440+
children: 'multiple',
441+
platform: ['macos'],
442+
fallback: 'section',
443+
}],
444+
['craft-sidebar-item', {
445+
name: 'SidebarItem',
446+
zigType: 'craft.NativeSidebarItem',
447+
props: [
448+
{ name: 'id', type: 'string', required: true },
449+
{ name: 'label', type: 'string', required: true },
450+
{ name: 'icon', type: 'string', required: false },
451+
{ name: 'badge', type: 'string', required: false },
452+
{ name: 'tintColor', type: 'color', required: false },
453+
],
454+
children: 'none',
455+
platform: ['macos'],
456+
fallback: 'button',
457+
}],
412458
['craft-accordion', {
413459
name: 'Accordion',
414460
zigType: 'craft.Accordion',

0 commit comments

Comments
 (0)