Changeset 0.37.19 (#232)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
50
CHANGELOG.md
50
CHANGELOG.md
@@ -1,5 +1,55 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.37.19.0] — 2026-03-24
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Tag release — "UI Complete." Closes all remaining code review items from
|
||||||
|
FE-REWRITE-REVIEW-0.37.14.md. Adds client-side RBAC gates (`sw.can()`) to
|
||||||
|
surface action buttons, migrates settings policy reads from `__PAGE_DATA__`
|
||||||
|
to `sw.auth.policies`, removes dead `__USER__` / `__PAGE_DATA__` template
|
||||||
|
injections, and moves shell-coupled `_getScale()` into the SDK. Light mode
|
||||||
|
CSS audit and dead code sweep confirm no outstanding issues.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **sw.can() RBAC gates (CR P2-1):** Channel/group creation buttons in chat
|
||||||
|
dashboard and sidebar gated behind `sw.can('channel.create')`. Persona
|
||||||
|
create/edit/delete gated behind `sw.can('persona.create')` /
|
||||||
|
`sw.can('persona.manage')`. Knowledge base create/upload/delete gated
|
||||||
|
behind `sw.can('kb.create')` / `sw.can('kb.write')`.
|
||||||
|
(`chat-workspace.js`, `sidebar.js`, `personas.js`, `knowledge.js`)
|
||||||
|
- **Settings → sw.auth.policies (CR P2-4):** Settings surface reads BYOK and
|
||||||
|
persona policies from `sw.auth.policies` (populated at SDK boot via
|
||||||
|
`/api/v1/profile/permissions`) instead of `window.__PAGE_DATA__`. Listens
|
||||||
|
for `auth.permissions.changed` event to react to policy updates.
|
||||||
|
(`settings/index.js`)
|
||||||
|
- **_getScale → sw.shell.getScale() (CR P3-3):** Scale detection for CSS zoom
|
||||||
|
compensation moved from sidebar-chats.js local function into SDK shell
|
||||||
|
namespace. Explicit coupling replaces implicit `#surfaceInner` DOM reach.
|
||||||
|
(`sdk/index.js`, `sidebar-chats.js`)
|
||||||
|
- **SDK version:** `sw._sdk` updated to `'0.37.19'`.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- **__USER__ / __PAGE_DATA__ injection (CR P2-4):** Removed `window.__USER__`
|
||||||
|
and `window.__PAGE_DATA__` global injections from `base.html`. SDK boot
|
||||||
|
populates `sw.auth.user` and `sw.auth.policies` from API. Extension packages
|
||||||
|
updated to use SDK (`editor`, `hello-dashboard`, `icd-test-runner`).
|
||||||
|
(`base.html`, `loaders.go`, `EXTENSION-SURFACES.md`)
|
||||||
|
- **SettingsPageData policy fields:** `BYOKEnabled` and `UserPersonasEnabled`
|
||||||
|
removed from Go struct (no longer injected via template). (`loaders.go`)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- **CR P3-17:** Added comment documenting intentional raw DOM usage in notes
|
||||||
|
markdown wikilink processing (`markdown.js`).
|
||||||
|
- **CR P3-19:** Added comment documenting keyboard shortcut DOM presence check
|
||||||
|
pattern with future guidance (`use-notes.js`).
|
||||||
|
- **EXTENSION-SURFACES.md:** Updated `__USER__` references to `sw.auth.user`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.37.18.0] — 2026-03-24
|
## [0.37.18.0] — 2026-03-24
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|||||||
@@ -122,9 +122,9 @@ The CI syntax check globs `src/js/*.js` (top-level only), missing all 115 files
|
|||||||
|
|
||||||
## P2 — Track for Next Changeset
|
## P2 — Track for Next Changeset
|
||||||
|
|
||||||
### 9. `sw.can()` is essentially unused
|
### 9. `sw.can()` is essentially unused — **RESOLVED v0.37.19**
|
||||||
|
|
||||||
Only 1 reference in all surface code (`team-admin/index.js` uses `sw.isAdmin`, not `sw.can()`). No surface gates create/edit/delete buttons with `sw.can('kb.write')`, `sw.can('channel.create')`, etc. The backend permission enforcement from CS-0.37.1 has no client-side counterpart.
|
~~Only 1 reference in all surface code (`team-admin/index.js` uses `sw.isAdmin`, not `sw.can()`). No surface gates create/edit/delete buttons with `sw.can('kb.write')`, `sw.can('channel.create')`, etc. The backend permission enforcement from CS-0.37.1 has no client-side counterpart.~~
|
||||||
|
|
||||||
The settings surface does its own policy resolution from `__PAGE_DATA__` + a separate API fallback, rather than reading `sw.auth.policies` which is already cached from `/profile/permissions`.
|
The settings surface does its own policy resolution from `__PAGE_DATA__` + a separate API fallback, rather than reading `sw.auth.policies` which is already cached from `/profile/permissions`.
|
||||||
|
|
||||||
@@ -148,11 +148,11 @@ The `useEffect` that sets `handleRef.current = {...}` has no dependency array, r
|
|||||||
|
|
||||||
**File:** `src/js/sw/components/chat-pane/index.js`, line 66
|
**File:** `src/js/sw/components/chat-pane/index.js`, line 66
|
||||||
|
|
||||||
### 12. `__USER__` / `__PAGE_DATA__` injection is mostly dead
|
### 12. `__USER__` / `__PAGE_DATA__` injection is mostly dead — **RESOLVED v0.37.19**
|
||||||
|
|
||||||
The Go template still injects `window.__USER__` and `window.__PAGE_DATA__` on every page load. The new SDK boots from API calls. Only `settings/index.js` reads `__PAGE_DATA__` (for BYOK/persona policies) and it already has an API fallback. `window.__USER__` is never read by new code.
|
~~The Go template still injects `window.__USER__` and `window.__PAGE_DATA__` on every page load. The new SDK boots from API calls. Only `settings/index.js` reads `__PAGE_DATA__` (for BYOK/persona policies) and it already has an API fallback. `window.__USER__` is never read by new code.~~
|
||||||
|
|
||||||
**Recommendation:** Remove `__USER__` injection. Migrate settings to use `sw.auth.policies` (see #9), then remove `__PAGE_DATA__` injection.
|
~~**Recommendation:** Remove `__USER__` injection. Migrate settings to use `sw.auth.policies` (see #9), then remove `__PAGE_DATA__` injection.~~
|
||||||
|
|
||||||
### 13. Debug modal is old-world code
|
### 13. Debug modal is old-world code
|
||||||
|
|
||||||
@@ -178,11 +178,11 @@ The debug modal in `base.html` (~50 lines of raw HTML with inline `onclick` hand
|
|||||||
|
|
||||||
**File:** `src/js/sw/surfaces/admin/users.js`, line 7
|
**File:** `src/js/sw/surfaces/admin/users.js`, line 7
|
||||||
|
|
||||||
### 16. `sidebar-chats.js` reaches into `#surfaceInner`
|
### 16. `sidebar-chats.js` reaches into `#surfaceInner` — **RESOLVED v0.37.19**
|
||||||
|
|
||||||
`_getScale()` reads `document.getElementById('surfaceInner')` to compensate for CSS zoom on `position: fixed` menus. Technically correct but creates an implicit coupling between a surface component and the shell's DOM structure.
|
~~`_getScale()` reads `document.getElementById('surfaceInner')` to compensate for CSS zoom on `position: fixed` menus. Technically correct but creates an implicit coupling between a surface component and the shell's DOM structure.~~
|
||||||
|
|
||||||
**Recommendation:** Move scale detection into the SDK (e.g., `sw.shell.getScale()`) so the coupling is explicit.
|
~~**Recommendation:** Move scale detection into the SDK (e.g., `sw.shell.getScale()`) so the coupling is explicit.~~
|
||||||
|
|
||||||
### 17. Notes markdown rendering uses raw DOM
|
### 17. Notes markdown rendering uses raw DOM
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ mounts into the `#extension-mount` container:
|
|||||||
if (!mount) return;
|
if (!mount) return;
|
||||||
|
|
||||||
var manifest = window.__MANIFEST__ || {};
|
var manifest = window.__MANIFEST__ || {};
|
||||||
var user = window.__USER__ || {};
|
var user = window.sw?.auth?.user || {};
|
||||||
|
|
||||||
mount.innerHTML = '<h1>Hello, ' + esc(user.display_name || user.username) + '!</h1>';
|
mount.innerHTML = '<h1>Hello, ' + esc(user.display_name || user.username) + '!</h1>';
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ available when your script executes:
|
|||||||
| Global | Type | Description |
|
| Global | Type | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| `window.__MANIFEST__` | Object | Your surface's manifest with route, layout, etc. Keys are **lowercase** (JSON serialization from Go). Access as `manifest.id`, `manifest.route`, `manifest.title`. |
|
| `window.__MANIFEST__` | Object | Your surface's manifest with route, layout, etc. Keys are **lowercase** (JSON serialization from Go). Access as `manifest.id`, `manifest.route`, `manifest.title`. |
|
||||||
| `window.__USER__` | Object | Authenticated user: `{ username, display_name, role, id, email }`. |
|
| `window.sw.auth.user` | Object | Authenticated user (via SDK): `{ username, display_name, role, id, email }`. Requires SDK boot. |
|
||||||
| `window.__BASE__` | String | URL base path (e.g. `"/dev"` or `""`). Prepend to all internal links. |
|
| `window.__BASE__` | String | URL base path (e.g. `"/dev"` or `""`). Prepend to all internal links. |
|
||||||
| `window.__VERSION__` | String | Platform version string. |
|
| `window.__VERSION__` | String | Platform version string. |
|
||||||
| `window.__SURFACE__` | String | Your surface ID. |
|
| `window.__SURFACE__` | String | Your surface ID. |
|
||||||
@@ -280,7 +280,7 @@ When your surface loads, the page DOM looks like this:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>window.__MANIFEST__ = {...};</script>
|
<script>window.__MANIFEST__ = {...};</script>
|
||||||
<script>window.__USER__ = {...};</script>
|
<!-- User available via sw.auth.user after SDK boot -->
|
||||||
<script src="/js/api.js"></script>
|
<script src="/js/api.js"></script>
|
||||||
<script src="/js/ui-core.js"></script>
|
<script src="/js/ui-core.js"></script>
|
||||||
... platform scripts ...
|
... platform scripts ...
|
||||||
@@ -447,7 +447,7 @@ working example that demonstrates:
|
|||||||
|
|
||||||
- Manifest structure
|
- Manifest structure
|
||||||
- Mounting into `#extension-mount`
|
- Mounting into `#extension-mount`
|
||||||
- Reading `__MANIFEST__` and `__USER__`
|
- Reading `__MANIFEST__` and `sw.auth.user`
|
||||||
- Using `UI.toast()` for notifications
|
- Using `UI.toast()` for notifications
|
||||||
- Using `API._get()` for authenticated requests
|
- Using `API._get()` for authenticated requests
|
||||||
- Using CSS custom properties for theming
|
- Using CSS custom properties for theming
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ v0.9.x–v0.28.7 Foundation through Platform Polish ✅
|
|||||||
│ v0.37.4 Shell ✅ │
|
│ v0.37.4 Shell ✅ │
|
||||||
│ v0.37.5–16 Surfaces ✅ │
|
│ v0.37.5–16 Surfaces ✅ │
|
||||||
│ v0.37.17–18 Surfaces ✅ │
|
│ v0.37.17–18 Surfaces ✅ │
|
||||||
│ v0.37.19 Tag │
|
│ v0.37.19 Tag ✅ │
|
||||||
│ │ │
|
│ │ │
|
||||||
v0.38.0–.4 │ │
|
v0.38.0–.4 │ │
|
||||||
Extension │ │
|
Extension │ │
|
||||||
@@ -95,11 +95,12 @@ v0.9.x–v0.28.7 Foundation through Platform Polish ✅
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## UI Rewrite Track
|
## UI Rewrite Track ✅
|
||||||
|
|
||||||
Scorched earth rebuild of the frontend on Preact+htm. Three-layer
|
Scorched earth rebuild of the frontend on Preact+htm. Three-layer
|
||||||
architecture (Primitives → SDK → Shell) replacing the imperative
|
architecture (Primitives → SDK → Shell) replacing the imperative
|
||||||
DOM manipulation codebase. See `UI redesign.md` for full design doc.
|
DOM manipulation codebase. See `UI redesign.md` for full design doc.
|
||||||
|
**Complete as of v0.37.19.**
|
||||||
|
|
||||||
Depends on: v0.36.0 (OpenAPI spec), v0.31.2 (SDK composability).
|
Depends on: v0.36.0 (OpenAPI spec), v0.31.2 (SDK composability).
|
||||||
|
|
||||||
@@ -204,7 +205,7 @@ Each surface rebuilt as Preact component tree. Old JS deleted per surface.
|
|||||||
| v0.37.16 | Projects surface ✅ | Card grid + detail, inline ChatPane, context menu, sidebar pickers, deep-link |
|
| v0.37.16 | Projects surface ✅ | Card grid + detail, inline ChatPane, context menu, sidebar pickers, deep-link |
|
||||||
| v0.37.17 | Workspaces ✅ | Workspace-first project file storage, tree browser UI, archive upload/download |
|
| v0.37.17 | Workspaces ✅ | Workspace-first project file storage, tree browser UI, archive upload/download |
|
||||||
| v0.37.18 | Debug / model surface ✅ | Debug modal Preact rebuild (CR P2-5), default workspace, tool_output auto-save, save-to-workspace bridge, model/provider polish |
|
| v0.37.18 | Debug / model surface ✅ | Debug modal Preact rebuild (CR P2-5), default workspace, tool_output auto-save, save-to-workspace bridge, model/provider polish |
|
||||||
| v0.37.19 | Tag | Light mode CSS audit, dead code hunt, all features verified; `sw.can()` RBAC gates across surfaces (CR P2-1), `__USER__`/`__PAGE_DATA__` removal (CR P2-4), `_getScale` → `sw.shell.getScale()` (CR P3-3) |
|
| v0.37.19 | Tag ✅ | `sw.can()` RBAC gates (CR P2-1), `__USER__`/`__PAGE_DATA__` removal (CR P2-4), `_getScale` → `sw.shell.getScale()` (CR P3-3), light mode CSS audit, dead code sweep |
|
||||||
|
|
||||||
**v0.37.14 — Scorched Earth IV + Pane Audit ✅** (10 sessions, v0.37.14.0–.22):
|
**v0.37.14 — Scorched Earth IV + Pane Audit ✅** (10 sessions, v0.37.14.0–.22):
|
||||||
|
|
||||||
@@ -255,10 +256,14 @@ auto-save (file refs linked to assistant messages). Save-to-workspace bridge
|
|||||||
(save code blocks to workspace). Model selector health dots. Provider test
|
(save code blocks to workspace). Model selector health dots. Provider test
|
||||||
buttons (BYOK + admin). repl.js absorbed into Preact. sw-debug.css extracted.
|
buttons (BYOK + admin). repl.js absorbed into Preact. sw-debug.css extracted.
|
||||||
|
|
||||||
**v0.37.19 — Tag ("UI Complete"):**
|
**v0.37.19 — Tag ("UI Complete") ✅:**
|
||||||
|
|
||||||
`sw.can()` RBAC gates, `__USER__`/`__PAGE_DATA__` removal, `_getScale` SDK,
|
`sw.can()` RBAC gates on chat, settings, and KB surfaces (CR P2-1). Settings
|
||||||
light mode CSS audit, dead code hunt. Every capability has a UI.
|
policies migrated from `__PAGE_DATA__` to `sw.auth.policies`; `__USER__` and
|
||||||
|
`__PAGE_DATA__` injections removed from `base.html` (CR P2-4). `_getScale()`
|
||||||
|
moved to `sw.shell.getScale()` SDK (CR P3-3). Light mode CSS audit clean.
|
||||||
|
Dead code sweep clean. P3-17/P3-19 documented as intentional. Every
|
||||||
|
capability has a UI.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -193,7 +193,7 @@
|
|||||||
const name = window.prompt('Workspace name:');
|
const name = window.prompt('Workspace name:');
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
try {
|
try {
|
||||||
const userId = sw.user?.id || window.__USER__?.id;
|
const userId = sw.user?.id;
|
||||||
if (!userId) throw new Error('Not authenticated');
|
if (!userId) throw new Error('Not authenticated');
|
||||||
const resp = await API.createWorkspace({ name: name.trim(), owner_type: 'user', owner_id: userId });
|
const resp = await API.createWorkspace({ name: name.trim(), owner_type: 'user', owner_id: userId });
|
||||||
const newId = resp.id || resp.data?.id;
|
const newId = resp.id || resp.data?.id;
|
||||||
@@ -264,7 +264,7 @@
|
|||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = 'Creating\u2026';
|
btn.textContent = 'Creating\u2026';
|
||||||
try {
|
try {
|
||||||
const userId = sw.user?.id || window.__USER__?.id;
|
const userId = sw.user?.id;
|
||||||
if (!userId) throw new Error('Not authenticated');
|
if (!userId) throw new Error('Not authenticated');
|
||||||
const resp = await API.createWorkspace({ name, owner_type: 'user', owner_id: userId });
|
const resp = await API.createWorkspace({ name, owner_type: 'user', owner_id: userId });
|
||||||
const newId = resp.id || resp.data?.id;
|
const newId = resp.id || resp.data?.id;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Platform contract:
|
* Platform contract:
|
||||||
* - Mounts into #extension-mount
|
* - Mounts into #extension-mount
|
||||||
* - window.__MANIFEST__ — surface manifest (json, lowercase keys)
|
* - window.__MANIFEST__ — surface manifest (json, lowercase keys)
|
||||||
* - window.__USER__ — authenticated user {username, display_name, role}
|
* - sw.auth.user — authenticated user (from SDK boot)
|
||||||
* - UI.toast(msg, type) — platform toast (success/error/info/warning)
|
* - UI.toast(msg, type) — platform toast (success/error/info/warning)
|
||||||
* - API._get(path) — authenticated fetch (returns parsed JSON)
|
* - API._get(path) — authenticated fetch (returns parsed JSON)
|
||||||
*/
|
*/
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
if (!mount) return;
|
if (!mount) return;
|
||||||
|
|
||||||
var manifest = window.__MANIFEST__ || {};
|
var manifest = window.__MANIFEST__ || {};
|
||||||
var user = window.__USER__ || {};
|
var user = window.sw?.auth?.user || {};
|
||||||
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||||
var name = user.display_name || user.username || 'World';
|
var name = user.display_name || user.username || 'World';
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
window.ICD = {
|
window.ICD = {
|
||||||
mount: mount,
|
mount: mount,
|
||||||
manifest: window.__MANIFEST__ || {},
|
manifest: window.__MANIFEST__ || {},
|
||||||
user: window.sw?.auth?.user || window.__USER__ || {},
|
user: window.sw?.auth?.user || {},
|
||||||
base: window.__BASE__ || '',
|
base: window.__BASE__ || '',
|
||||||
|
|
||||||
// State (populated by framework.js)
|
// State (populated by framework.js)
|
||||||
|
|||||||
@@ -312,6 +312,7 @@ func (h *ChannelHandler) CreateChannel(c *gin.Context) {
|
|||||||
|
|
||||||
if err := h.stores.Channels.CreateFull(ctx, ch, req.Folder, req.Tags, aiMode,
|
if err := h.stores.Channels.CreateFull(ctx, ch, req.Folder, req.Tags, aiMode,
|
||||||
userID, dmPartners, req.Model, defaultConfigID); err != nil {
|
userID, dmPartners, req.Model, defaultConfigID); err != nil {
|
||||||
|
log.Printf("[channels] CreateFull error: %v", err)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create channel"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create channel"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,13 +108,9 @@ type NotesPageData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SettingsPageData is what the settings surface templates receive.
|
// SettingsPageData is what the settings surface templates receive.
|
||||||
|
// Feature gates (BYOK, personas) moved to sw.auth.policies in v0.37.19.
|
||||||
type SettingsPageData struct {
|
type SettingsPageData struct {
|
||||||
Section string `json:"section"`
|
Section string `json:"section"`
|
||||||
|
|
||||||
// v0.22.7: Feature gates from admin config.
|
|
||||||
// These control which nav links/tabs are visible in the settings surface.
|
|
||||||
BYOKEnabled bool `json:"BYOKEnabled"`
|
|
||||||
UserPersonasEnabled bool `json:"UserPersonasEnabled"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Loader registration ──────────────────────
|
// ── Loader registration ──────────────────────
|
||||||
@@ -422,20 +418,7 @@ func (e *Engine) settingsLoader(c *gin.Context, s store.Stores) (any, error) {
|
|||||||
section = "general"
|
section = "general"
|
||||||
}
|
}
|
||||||
|
|
||||||
data := &SettingsPageData{Section: section}
|
return &SettingsPageData{Section: section}, nil
|
||||||
|
|
||||||
// Read feature gates from policies (where admin settings saves them).
|
|
||||||
// Keys: allow_user_byok, allow_user_personas
|
|
||||||
if s.Policies != nil {
|
|
||||||
ctx := context.Background()
|
|
||||||
policies, err := s.Policies.GetAll(ctx)
|
|
||||||
if err == nil {
|
|
||||||
data.BYOKEnabled = policies["allow_user_byok"] == "true"
|
|
||||||
data.UserPersonasEnabled = policies["allow_user_personas"] == "true"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) projectsLoader(c *gin.Context, s store.Stores) (any, error) {
|
func (e *Engine) projectsLoader(c *gin.Context, s store.Stores) (any, error) {
|
||||||
|
|||||||
@@ -121,8 +121,8 @@
|
|||||||
window.__ENV__ = '{{.Environment}}';
|
window.__ENV__ = '{{.Environment}}';
|
||||||
window.__BRANDING__ = {};
|
window.__BRANDING__ = {};
|
||||||
window.__SURFACE__ = '{{.Surface}}';
|
window.__SURFACE__ = '{{.Surface}}';
|
||||||
window.__PAGE_DATA__ = {{.Data | toJSON}};
|
{{/* __PAGE_DATA__ and __USER__ removed in v0.37.19 — SDK boot
|
||||||
window.__USER__ = {{.User | toJSON}};
|
populates sw.auth.user and sw.auth.policies from API. */}}
|
||||||
{{if .Manifest}}window.__MANIFEST__ = {{.Manifest | toJSON}};{{end}}
|
{{if .Manifest}}window.__MANIFEST__ = {{.Manifest | toJSON}};{{end}}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sw-notes-surface__search {
|
input.sw-notes-surface__search {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 6px 8px 6px 30px;
|
padding: 6px 8px 6px 30px;
|
||||||
background: var(--bg, #0e0e10);
|
background: var(--bg, #0e0e10);
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sw-notes-surface__search:focus {
|
input.sw-notes-surface__search:focus {
|
||||||
border-color: var(--accent, #b38a4e);
|
border-color: var(--accent, #b38a4e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
// then post-processes wikilinks into clickable chips
|
// then post-processes wikilinks into clickable chips
|
||||||
// and transclusion blocks.
|
// and transclusion blocks.
|
||||||
// Independently importable.
|
// Independently importable.
|
||||||
|
//
|
||||||
|
// NOTE (CR P3-17): Uses raw DOM (innerHTML, querySelector, addEventListener)
|
||||||
|
// for post-render wikilink processing. This is intentional — Preact renders
|
||||||
|
// the initial HTML, then this module post-processes specific elements for
|
||||||
|
// interactive behavior. The lifecycle is managed by the parent component.
|
||||||
|
|
||||||
const WIKILINK_RE = /(!?)\[\[([^\[\]|]+?)(?:\|([^\]]+?))?\]\]/g;
|
const WIKILINK_RE = /(!?)\[\[([^\[\]|]+?)(?:\|([^\]]+?))?\]\]/g;
|
||||||
|
|
||||||
|
|||||||
@@ -257,6 +257,8 @@ export function useNotes(opts = {}) {
|
|||||||
toast('Note updated', 'success');
|
toast('Note updated', 'success');
|
||||||
setEditorMode('read');
|
setEditorMode('read');
|
||||||
_setView('reader');
|
_setView('reader');
|
||||||
|
// Notify sidebar to refresh folders/tags (folder or tags may have changed)
|
||||||
|
sw.emit('note.updated', { id: editingId }, { localOnly: true });
|
||||||
// Reload backlinks
|
// Reload backlinks
|
||||||
try {
|
try {
|
||||||
const bl = await api().backlinks(editingId);
|
const bl = await api().backlinks(editingId);
|
||||||
@@ -269,6 +271,8 @@ export function useNotes(opts = {}) {
|
|||||||
toast('Note created', 'success');
|
toast('Note created', 'success');
|
||||||
setEditorMode('read');
|
setEditorMode('read');
|
||||||
_setView('reader');
|
_setView('reader');
|
||||||
|
// Notify sidebar to refresh folders/tags
|
||||||
|
sw.emit('note.created', { id: created.id }, { localOnly: true });
|
||||||
}
|
}
|
||||||
graphDirtyRef.current = true;
|
graphDirtyRef.current = true;
|
||||||
return true;
|
return true;
|
||||||
@@ -289,6 +293,7 @@ export function useNotes(opts = {}) {
|
|||||||
setView('list');
|
setView('list');
|
||||||
refreshList();
|
refreshList();
|
||||||
_loadFolders();
|
_loadFolders();
|
||||||
|
sw.emit('note.deleted', { id: editingId }, { localOnly: true });
|
||||||
} catch (e) { toast(e.message, 'error'); }
|
} catch (e) { toast(e.message, 'error'); }
|
||||||
}, [editingId, setView, refreshList, _loadFolders]);
|
}, [editingId, setView, refreshList, _loadFolders]);
|
||||||
|
|
||||||
@@ -417,7 +422,9 @@ export function useNotes(opts = {}) {
|
|||||||
|
|
||||||
// Ctrl/Cmd + N: new note
|
// Ctrl/Cmd + N: new note
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
||||||
// Only prevent when notes pane is focused context
|
// DOM presence check gates shortcut to notes surface (CR P3-19).
|
||||||
|
// If notes and chat are ever composed on the same page, revisit
|
||||||
|
// with a focus-based check or sw.shell.activeSurface flag.
|
||||||
if (document.querySelector('.sw-notes-pane')) {
|
if (document.querySelector('.sw-notes-pane')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
newNote();
|
newNote();
|
||||||
|
|||||||
@@ -97,6 +97,19 @@ export async function boot() {
|
|||||||
sw.confirm = confirm;
|
sw.confirm = confirm;
|
||||||
sw.prompt = prompt;
|
sw.prompt = prompt;
|
||||||
|
|
||||||
|
// Shell — layout utilities (decouples surface code from shell DOM)
|
||||||
|
sw.shell = Object.freeze({
|
||||||
|
/** CSS transform scale on #surfaceInner (appearance zoom). */
|
||||||
|
getScale() {
|
||||||
|
const el = document.getElementById('surfaceInner');
|
||||||
|
if (!el) return 1;
|
||||||
|
const t = getComputedStyle(el).transform;
|
||||||
|
if (!t || t === 'none') return 1;
|
||||||
|
const m = t.match(/matrix\(([^,]+)/);
|
||||||
|
return m ? parseFloat(m[1]) || 1 : 1;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Toast — dynamic import to avoid module resolution issues
|
// Toast — dynamic import to avoid module resolution issues
|
||||||
try {
|
try {
|
||||||
const toastMod = await import('../primitives/toast.js');
|
const toastMod = await import('../primitives/toast.js');
|
||||||
@@ -139,7 +152,7 @@ export async function boot() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Marker for idempotency
|
// Marker for idempotency
|
||||||
sw._sdk = '0.37.14';
|
sw._sdk = '0.37.19';
|
||||||
|
|
||||||
// 8. Expose globally
|
// 8. Expose globally
|
||||||
window.sw = sw;
|
window.sw = sw;
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ function ChatDashboard({ onNewChat, onCreateChannel, items, onNavigate }) {
|
|||||||
const _onDmSelect = useCallback((user) => {
|
const _onDmSelect = useCallback((user) => {
|
||||||
setShowDmPicker(false);
|
setShowDmPicker(false);
|
||||||
const name = user.display_name || user.username;
|
const name = user.display_name || user.username;
|
||||||
onCreateChannel('dm', 'DM: ' + name, { participant: user.username || user.id });
|
onCreateChannel('dm', 'DM: ' + name, { participant: user.id });
|
||||||
}, [onCreateChannel]);
|
}, [onCreateChannel]);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -186,14 +186,16 @@ function ChatDashboard({ onNewChat, onCreateChannel, items, onNavigate }) {
|
|||||||
<span class="sw-chat-dashboard__card-icon">\u{1F4AC}</span>
|
<span class="sw-chat-dashboard__card-icon">\u{1F4AC}</span>
|
||||||
<span class="sw-chat-dashboard__card-label">New AI Chat</span>
|
<span class="sw-chat-dashboard__card-label">New AI Chat</span>
|
||||||
</button>
|
</button>
|
||||||
|
${sw.can('channel.create') && html`
|
||||||
<button class="sw-chat-dashboard__card" onClick=${() => _create('channel')}>
|
<button class="sw-chat-dashboard__card" onClick=${() => _create('channel')}>
|
||||||
<span class="sw-chat-dashboard__card-icon">#</span>
|
<span class="sw-chat-dashboard__card-icon">#</span>
|
||||||
<span class="sw-chat-dashboard__card-label">New Channel</span>
|
<span class="sw-chat-dashboard__card-label">New Channel</span>
|
||||||
</button>
|
</button>`}
|
||||||
|
${sw.can('channel.create') && html`
|
||||||
<button class="sw-chat-dashboard__card" onClick=${() => _create('group')}>
|
<button class="sw-chat-dashboard__card" onClick=${() => _create('group')}>
|
||||||
<span class="sw-chat-dashboard__card-icon">\u{1F465}</span>
|
<span class="sw-chat-dashboard__card-icon">\u{1F465}</span>
|
||||||
<span class="sw-chat-dashboard__card-label">New Group</span>
|
<span class="sw-chat-dashboard__card-label">New Group</span>
|
||||||
</button>
|
</button>`}
|
||||||
<button class="sw-chat-dashboard__card" onClick=${() => _create('dm')}>
|
<button class="sw-chat-dashboard__card" onClick=${() => _create('dm')}>
|
||||||
<span class="sw-chat-dashboard__card-icon">\u{1F4E8}</span>
|
<span class="sw-chat-dashboard__card-icon">\u{1F4E8}</span>
|
||||||
<span class="sw-chat-dashboard__card-label">Direct Message</span>
|
<span class="sw-chat-dashboard__card-label">Direct Message</span>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function SidebarItems({
|
|||||||
const _openMenu = useCallback((e, type, item) => {
|
const _openMenu = useCallback((e, type, item) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const scale = _getScale();
|
const scale = sw.shell.getScale();
|
||||||
// The dots button may be display:none (zero rect) — fall back to parent
|
// The dots button may be display:none (zero rect) — fall back to parent
|
||||||
let rect = e.currentTarget.getBoundingClientRect();
|
let rect = e.currentTarget.getBoundingClientRect();
|
||||||
if (!rect.width && !rect.height) {
|
if (!rect.width && !rect.height) {
|
||||||
@@ -97,7 +97,7 @@ export function SidebarItems({
|
|||||||
const _bodyContextMenu = useCallback((e) => {
|
const _bodyContextMenu = useCallback((e) => {
|
||||||
if (e.target.closest('.sw-chat-surface__item') || e.target.closest('.sw-chat-surface__folder-header')) return;
|
if (e.target.closest('.sw-chat-surface__item') || e.target.closest('.sw-chat-surface__folder-header')) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const scale = _getScale();
|
const scale = sw.shell.getScale();
|
||||||
setContextMenu({ type: 'body', item: null, x: e.clientX / scale, y: e.clientY / scale });
|
setContextMenu({ type: 'body', item: null, x: e.clientX / scale, y: e.clientY / scale });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -356,16 +356,6 @@ function ChannelItem({ channel, activeId, onSelect, onOpenMenu, onDragStart, ind
|
|||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the CSS scale transform on .surface-inner (set by appearance zoom).
|
// Scale detection moved to sw.shell.getScale() in v0.37.19 (CR P3-3).
|
||||||
// Context menus use position:fixed which breaks under transforms,
|
|
||||||
// so we divide getBoundingClientRect values by scale to compensate.
|
|
||||||
function _getScale() {
|
|
||||||
const el = document.getElementById('surfaceInner');
|
|
||||||
if (!el) return 1;
|
|
||||||
const t = getComputedStyle(el).transform;
|
|
||||||
if (!t || t === 'none') return 1;
|
|
||||||
const m = t.match(/matrix\(([^,]+)/);
|
|
||||||
return m ? parseFloat(m[1]) || 1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Folder count = direct children only (subfolders + channels in this folder).
|
// Folder count = direct children only (subfolders + channels in this folder).
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { SidebarItems } from './sidebar-chats.js';
|
import { SidebarItems } from './sidebar-chats.js';
|
||||||
import { UserMenu } from '../../shell/user-menu.js';
|
import { UserMenu } from '../../shell/user-menu.js';
|
||||||
|
import { UserPicker } from '../../primitives/user-picker.js';
|
||||||
|
|
||||||
const html = window.html;
|
const html = window.html;
|
||||||
const { useState, useCallback, useRef, useEffect } = window.hooks;
|
const { useState, useCallback, useRef, useEffect } = window.hooks;
|
||||||
@@ -50,6 +51,7 @@ const FOLDER_PLUS_SVG = html`
|
|||||||
*/
|
*/
|
||||||
export function Sidebar({ sidebar, activeId, onSelect, onNewChat, onCreateChannel, onDeleteChat, onCollapse }) {
|
export function Sidebar({ sidebar, activeId, onSelect, onNewChat, onCreateChannel, onDeleteChat, onCollapse }) {
|
||||||
const [newMenuOpen, setNewMenuOpen] = useState(false);
|
const [newMenuOpen, setNewMenuOpen] = useState(false);
|
||||||
|
const [showDmPicker, setShowDmPicker] = useState(false);
|
||||||
const newMenuRef = useRef(null);
|
const newMenuRef = useRef(null);
|
||||||
|
|
||||||
const _onSearch = useCallback((e) => {
|
const _onSearch = useCallback((e) => {
|
||||||
@@ -82,14 +84,19 @@ export function Sidebar({ sidebar, activeId, onSelect, onNewChat, onCreateChanne
|
|||||||
if (type === 'direct') {
|
if (type === 'direct') {
|
||||||
onNewChat();
|
onNewChat();
|
||||||
} else if (type === 'dm') {
|
} else if (type === 'dm') {
|
||||||
const who = await window.sw.prompt('Username to DM:', '');
|
setShowDmPicker(true);
|
||||||
if (who && who.trim()) onCreateChannel('dm', 'DM: ' + who.trim(), { participant: who.trim() });
|
|
||||||
} else {
|
} else {
|
||||||
const name = await window.sw.prompt((type === 'channel' ? 'Channel' : 'Group') + ' name:', '');
|
const name = await window.sw.prompt((type === 'channel' ? 'Channel' : 'Group') + ' name:', '');
|
||||||
if (name && name.trim()) onCreateChannel(type, name.trim());
|
if (name && name.trim()) onCreateChannel(type, name.trim());
|
||||||
}
|
}
|
||||||
}, [onNewChat, onCreateChannel]);
|
}, [onNewChat, onCreateChannel]);
|
||||||
|
|
||||||
|
const _onDmSelect = useCallback((user) => {
|
||||||
|
setShowDmPicker(false);
|
||||||
|
const name = user.display_name || user.username;
|
||||||
|
onCreateChannel('dm', 'DM: ' + name, { participant: user.id });
|
||||||
|
}, [onCreateChannel]);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="sw-chat-surface__sidebar" role="navigation" aria-label="Chat sidebar">
|
<div class="sw-chat-surface__sidebar" role="navigation" aria-label="Chat sidebar">
|
||||||
${/* Header: new chat + collapse */''}
|
${/* Header: new chat + collapse */''}
|
||||||
@@ -107,12 +114,12 @@ export function Sidebar({ sidebar, activeId, onSelect, onNewChat, onCreateChanne
|
|||||||
<button onClick=${() => _newChannel('direct')}>
|
<button onClick=${() => _newChannel('direct')}>
|
||||||
<span class="sw-chat-surface__new-menu-icon">\u{1F4AC}</span> AI Chat
|
<span class="sw-chat-surface__new-menu-icon">\u{1F4AC}</span> AI Chat
|
||||||
</button>
|
</button>
|
||||||
<button onClick=${() => _newChannel('channel')}>
|
${sw.can('channel.create') && html`<button onClick=${() => _newChannel('channel')}>
|
||||||
<span class="sw-chat-surface__new-menu-icon">#</span> Channel
|
<span class="sw-chat-surface__new-menu-icon">#</span> Channel
|
||||||
</button>
|
</button>`}
|
||||||
<button onClick=${() => _newChannel('group')}>
|
${sw.can('channel.create') && html`<button onClick=${() => _newChannel('group')}>
|
||||||
<span class="sw-chat-surface__new-menu-icon">\u{1F465}</span> Group
|
<span class="sw-chat-surface__new-menu-icon">\u{1F465}</span> Group
|
||||||
</button>
|
</button>`}
|
||||||
<button onClick=${() => _newChannel('dm')}>
|
<button onClick=${() => _newChannel('dm')}>
|
||||||
<span class="sw-chat-surface__new-menu-icon">\u{1F4E8}</span> Direct Message
|
<span class="sw-chat-surface__new-menu-icon">\u{1F4E8}</span> Direct Message
|
||||||
</button>
|
</button>
|
||||||
@@ -132,6 +139,17 @@ export function Sidebar({ sidebar, activeId, onSelect, onNewChat, onCreateChanne
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${showDmPicker && html`
|
||||||
|
<div style="padding: 6px 8px;">
|
||||||
|
<${UserPicker}
|
||||||
|
onSelect=${_onDmSelect}
|
||||||
|
placeholder="Search for a user\u2026"
|
||||||
|
autoFocus=${true} />
|
||||||
|
<button class="btn-small"
|
||||||
|
style="margin-top: 4px; width: 100%;"
|
||||||
|
onClick=${() => setShowDmPicker(false)}>Cancel</button>
|
||||||
|
</div>`}
|
||||||
|
|
||||||
${/* Search */''}
|
${/* Search */''}
|
||||||
<div class="sw-chat-surface__search-wrap">
|
<div class="sw-chat-surface__search-wrap">
|
||||||
<span class="sw-chat-surface__search-icon">${SEARCH_SVG}</span>
|
<span class="sw-chat-surface__search-icon">${SEARCH_SVG}</span>
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
* Reads globals:
|
* Reads globals:
|
||||||
* __SECTION__ — active section name (string)
|
* __SECTION__ — active section name (string)
|
||||||
* __BASE__ — base path
|
* __BASE__ — base path
|
||||||
* __PAGE_DATA__ — { BYOKEnabled, ... } from Go template
|
*
|
||||||
|
* Policy flags (BYOK, personas) come from sw.auth.policies,
|
||||||
|
* populated at SDK boot via GET /api/v1/profile/permissions.
|
||||||
*
|
*
|
||||||
* Layout: topbar + left nav + content area (same CSS classes as before).
|
* Layout: topbar + left nav + content area (same CSS classes as before).
|
||||||
* All sections are native Preact components loaded lazily.
|
* All sections are native Preact components loaded lazily.
|
||||||
@@ -73,21 +75,21 @@ const SECTION_TITLES = {
|
|||||||
function SettingsSurface() {
|
function SettingsSurface() {
|
||||||
const BASE = window.__BASE__ || '';
|
const BASE = window.__BASE__ || '';
|
||||||
const section = window.__SECTION__ || 'general';
|
const section = window.__SECTION__ || 'general';
|
||||||
const pageData = window.__PAGE_DATA__ || {};
|
|
||||||
|
|
||||||
const [byokEnabled, setByokEnabled] = useState(!!pageData.BYOKEnabled);
|
const policies = sw.auth?.policies || {};
|
||||||
const [personasEnabled, setPersonasEnabled] = useState(!!pageData.UserPersonasEnabled);
|
const [byokEnabled, setByokEnabled] = useState(policies.allow_user_byok === 'true');
|
||||||
|
const [personasEnabled, setPersonasEnabled] = useState(policies.allow_user_personas === 'true');
|
||||||
const [SectionComponent, setSectionComponent] = useState(null);
|
const [SectionComponent, setSectionComponent] = useState(null);
|
||||||
|
|
||||||
// Fetch policies if not provided by template
|
// Update when permissions refresh (e.g. after boot or policy change)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pageData.BYOKEnabled || !pageData.UserPersonasEnabled) {
|
function _onPermsChanged() {
|
||||||
sw.api.admin?.settings?.public?.().then(data => {
|
const p = sw.auth?.policies || {};
|
||||||
const policies = data?.policies || {};
|
setByokEnabled(p.allow_user_byok === 'true');
|
||||||
if (policies.allow_user_byok === 'true') setByokEnabled(true);
|
setPersonasEnabled(p.allow_user_personas === 'true');
|
||||||
if (policies.allow_user_personas === 'true') setPersonasEnabled(true);
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
}
|
||||||
|
sw.on('auth.permissions.changed', _onPermsChanged);
|
||||||
|
return () => sw.off('auth.permissions.changed', _onPermsChanged);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Load the section component
|
// Load the section component
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ export default function KnowledgeSection() {
|
|||||||
return html`<span class="badge ${cls}">${status}</span>`;
|
return html`<span class="badge ${cls}">${status}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canCreate = sw.can('kb.create');
|
||||||
|
const canWrite = sw.can('kb.write');
|
||||||
|
|
||||||
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
|
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
|
||||||
|
|
||||||
// Detail view
|
// Detail view
|
||||||
@@ -111,8 +114,8 @@ export default function KnowledgeSection() {
|
|||||||
<h4 style="margin:0;">${detail.name}</h4>
|
<h4 style="margin:0;">${detail.name}</h4>
|
||||||
${statusBadge(detail.status || 'active')}
|
${statusBadge(detail.status || 'active')}
|
||||||
<div style="flex:1"></div>
|
<div style="flex:1"></div>
|
||||||
<button class="btn-small" onClick=${uploadDoc}>Upload Document</button>
|
${canWrite && html`<button class="btn-small" onClick=${uploadDoc}>Upload Document</button>`}
|
||||||
<button class="btn-small btn-danger" onClick=${() => deleteKb(detail.id)}>Delete KB</button>
|
${canWrite && html`<button class="btn-small btn-danger" onClick=${() => deleteKb(detail.id)}>Delete KB</button>`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${detail.description && html`
|
${detail.description && html`
|
||||||
@@ -132,7 +135,7 @@ export default function KnowledgeSection() {
|
|||||||
${' '}${statusBadge(d.status || 'pending')}
|
${' '}${statusBadge(d.status || 'pending')}
|
||||||
</div>
|
</div>
|
||||||
<span class="text-muted" style="font-size:11px;">${fmtDate(d.created_at)}</span>
|
<span class="text-muted" style="font-size:11px;">${fmtDate(d.created_at)}</span>
|
||||||
<button class="btn-small btn-danger" onClick=${() => deleteDoc(d.id)}>Delete</button>
|
${canWrite && html`<button class="btn-small btn-danger" onClick=${() => deleteDoc(d.id)}>Delete</button>`}
|
||||||
</div>
|
</div>
|
||||||
`)}
|
`)}
|
||||||
</div>
|
</div>
|
||||||
@@ -145,9 +148,10 @@ export default function KnowledgeSection() {
|
|||||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||||||
<span class="text-muted" style="font-size:12px;">${kbs.length} knowledge base${kbs.length !== 1 ? 's' : ''}</span>
|
<span class="text-muted" style="font-size:12px;">${kbs.length} knowledge base${kbs.length !== 1 ? 's' : ''}</span>
|
||||||
<div style="flex:1"></div>
|
<div style="flex:1"></div>
|
||||||
|
${canCreate && html`
|
||||||
<button class="btn-md btn-primary" onClick=${() => setShowCreate(!showCreate)}>
|
<button class="btn-md btn-primary" onClick=${() => setShowCreate(!showCreate)}>
|
||||||
${showCreate ? 'Cancel' : '+ Create'}
|
${showCreate ? 'Cancel' : '+ Create'}
|
||||||
</button>
|
</button>`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${showCreate && html`
|
${showCreate && html`
|
||||||
|
|||||||
@@ -85,10 +85,14 @@ export function PersonasSection() {
|
|||||||
return html`<div class="settings-placeholder">Loading personas\u2026</div>`;
|
return html`<div class="settings-placeholder">Loading personas\u2026</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canCreate = sw.can('persona.create');
|
||||||
|
const canManage = sw.can('persona.manage');
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
${canCreate && html`
|
||||||
<div style="margin-bottom:12px;">
|
<div style="margin-bottom:12px;">
|
||||||
<button class="btn-md btn-primary" onClick=${openAdd}>+ New Persona</button>
|
<button class="btn-md btn-primary" onClick=${openAdd}>+ New Persona</button>
|
||||||
</div>
|
</div>`}
|
||||||
|
|
||||||
${showForm && html`
|
${showForm && html`
|
||||||
<div class="settings-section" style="margin-bottom:16px;">
|
<div class="settings-section" style="margin-bottom:16px;">
|
||||||
@@ -136,6 +140,7 @@ export function PersonasSection() {
|
|||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
|
${canManage && html`
|
||||||
<div style="display:flex;gap:4px;">
|
<div style="display:flex;gap:4px;">
|
||||||
<button class="icon-btn" title="Edit" onClick=${() => openEdit(p)}>
|
<button class="icon-btn" title="Edit" onClick=${() => openEdit(p)}>
|
||||||
\u{270F}\u{FE0F}
|
\u{270F}\u{FE0F}
|
||||||
@@ -144,7 +149,7 @@ export function PersonasSection() {
|
|||||||
onClick=${() => del(p)}>
|
onClick=${() => del(p)}>
|
||||||
\u{1F5D1}
|
\u{1F5D1}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`)}
|
`)}
|
||||||
|
|||||||
Reference in New Issue
Block a user