smoodly 0.0.12 → 0.0.13
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/README.md +219 -25
- package/dist/admin/editor/CanvasOverlay.d.ts +6 -2
- package/dist/admin/editor/CanvasOverlay.js +9 -11
- package/dist/admin/editor/EditorView.js +130 -25
- package/dist/admin/editor/FloatingToolbar.d.ts +16 -0
- package/dist/admin/editor/FloatingToolbar.js +14 -0
- package/dist/admin/editor/InlineRichtext.d.ts +9 -0
- package/dist/admin/editor/InlineRichtext.js +50 -0
- package/dist/admin/editor/Outline.d.ts +16 -10
- package/dist/admin/editor/Outline.js +13 -10
- package/dist/admin/editor/inline-dom.d.ts +27 -0
- package/dist/admin/editor/inline-dom.js +43 -0
- package/dist/admin/editor/inline-session.d.ts +30 -0
- package/dist/admin/editor/inline-session.js +19 -0
- package/dist/admin/editor/protocol.d.ts +22 -1
- package/dist/admin/editor/useCanvasBridge.d.ts +8 -4
- package/dist/admin/editor/useCanvasBridge.js +12 -7
- package/dist/admin/editor/useInlineSession.d.ts +46 -0
- package/dist/admin/editor/useInlineSession.js +244 -0
- package/dist/admin/field-paths.d.ts +13 -0
- package/dist/admin/field-paths.js +49 -0
- package/dist/admin/forms/FieldWidget.d.ts +7 -2
- package/dist/admin/forms/FieldWidget.js +12 -40
- package/dist/admin/forms/ListField.d.ts +10 -0
- package/dist/admin/forms/ListField.js +42 -0
- package/dist/admin/forms/MediaWidget.js +10 -3
- package/dist/admin/forms/RichtextElementCard.d.ts +3 -0
- package/dist/admin/forms/RichtextElementCard.js +37 -0
- package/dist/admin/forms/RichtextImageCard.d.ts +3 -0
- package/dist/admin/forms/RichtextImageCard.js +42 -0
- package/dist/admin/forms/RichtextInsertMenu.d.ts +6 -0
- package/dist/admin/forms/RichtextInsertMenu.js +6 -0
- package/dist/admin/forms/RichtextPopovers.d.ts +7 -0
- package/dist/admin/forms/RichtextPopovers.js +25 -0
- package/dist/admin/forms/RichtextToolbar.d.ts +4 -2
- package/dist/admin/forms/RichtextToolbar.js +4 -2
- package/dist/admin/forms/RichtextWidget.d.ts +4 -1
- package/dist/admin/forms/RichtextWidget.js +23 -120
- package/dist/admin/forms/elements-context.d.ts +7 -0
- package/dist/admin/forms/elements-context.js +13 -0
- package/dist/admin/forms/reveal.d.ts +5 -0
- package/dist/admin/forms/reveal.js +47 -0
- package/dist/admin/forms/richtext-extensions.d.ts +8 -0
- package/dist/admin/forms/richtext-extensions.js +11 -0
- package/dist/admin/forms/richtext-form-context.d.ts +16 -0
- package/dist/admin/forms/richtext-form-context.js +18 -0
- package/dist/admin/forms/richtext-insert.d.ts +38 -0
- package/dist/admin/forms/richtext-insert.js +37 -0
- package/dist/admin/forms/richtext-nodes.d.ts +37 -0
- package/dist/admin/forms/richtext-nodes.js +85 -0
- package/dist/admin/forms/richtext-paste.js +5 -0
- package/dist/admin/forms/tabs.d.ts +3 -0
- package/dist/admin/forms/tabs.js +6 -0
- package/dist/admin/forms/useRichtextEditor.d.ts +51 -0
- package/dist/admin/forms/useRichtextEditor.js +281 -0
- package/dist/admin/next/bridge.js +51 -20
- package/dist/admin/ops-impl.js +13 -8
- package/dist/admin/richtext-doc.d.ts +3 -0
- package/dist/admin/richtext-doc.js +12 -0
- package/dist/admin/serialize.d.ts +7 -0
- package/dist/admin/serialize.js +16 -1
- package/dist/admin/shell/AdminApp.js +2 -1
- package/dist/admin/shell/EntriesList.js +34 -12
- package/dist/admin/shell/EntryForm.js +22 -15
- package/dist/admin/shell/IconRail.js +6 -5
- package/dist/admin/shell/ListView.d.ts +9 -2
- package/dist/admin/shell/ListView.js +7 -6
- package/dist/admin/shell/LoginView.js +2 -2
- package/dist/admin/shell/PagesList.js +53 -24
- package/dist/admin/shell/SharedForm.js +17 -11
- package/dist/admin/shell/SharedList.js +5 -3
- package/dist/admin/shell/entries-list.d.ts +4 -0
- package/dist/admin/shell/entries-list.js +6 -0
- package/dist/admin/shell/pages-tree.d.ts +24 -0
- package/dist/admin/shell/pages-tree.js +59 -1
- package/dist/admin/tree-ops.d.ts +9 -1
- package/dist/admin/tree-ops.js +27 -0
- package/dist/admin/ui/LocaleSwitcher.d.ts +10 -0
- package/dist/admin/ui/LocaleSwitcher.js +28 -8
- package/dist/admin/ui/Modal.js +14 -2
- package/dist/admin/ui/ResizeHandle.d.ts +1 -0
- package/dist/admin/ui/ResizeHandle.js +127 -0
- package/dist/admin/ui/primitives.d.ts +29 -4
- package/dist/admin/ui/primitives.js +29 -10
- package/dist/admin/ui/resize.d.ts +16 -0
- package/dist/admin/ui/resize.js +23 -0
- package/dist/admin/ui/sortable.d.ts +23 -0
- package/dist/admin/ui/sortable.js +57 -0
- package/dist/admin/ui/theme.d.ts +3 -1
- package/dist/admin/ui/theme.js +512 -165
- package/dist/admin/ui/useSortable.d.ts +35 -0
- package/dist/admin/ui/useSortable.js +220 -0
- package/dist/admin/validate.d.ts +6 -1
- package/dist/admin/validate.js +28 -9
- package/dist/config.d.ts +1 -0
- package/dist/config.js +36 -0
- package/dist/entry-store.d.ts +5 -2
- package/dist/entry-store.js +4 -3
- package/dist/fields.d.ts +4 -0
- package/dist/fields.js +2 -2
- package/dist/image/SmoodlyImage.d.ts +4 -1
- package/dist/image/SmoodlyImage.js +2 -2
- package/dist/index.d.ts +8 -1
- package/dist/index.js +5 -1
- package/dist/marks.d.ts +53 -0
- package/dist/marks.js +43 -0
- package/dist/media.d.ts +3 -2
- package/dist/media.js +48 -6
- package/dist/refs.d.ts +2 -2
- package/dist/refs.js +33 -12
- package/dist/render.js +2 -1
- package/dist/resolve.d.ts +8 -2
- package/dist/resolve.js +8 -7
- package/dist/richtext/index.d.ts +12 -0
- package/dist/richtext/index.js +9 -0
- package/dist/richtext-render.d.ts +22 -2
- package/dist/richtext-render.js +40 -13
- package/dist/richtext-walk.d.ts +44 -0
- package/dist/richtext-walk.js +46 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +13 -1
- package/dist/section-wrapper.js +2 -1
- package/dist/site.d.ts +2 -0
- package/dist/site.js +7 -4
- package/dist/store.d.ts +3 -1
- package/dist/store.js +4 -4
- package/dist/supabase-entry-store.d.ts +1 -0
- package/dist/supabase-entry-store.js +2 -1
- package/dist/supabase-store.js +1 -1
- package/dist/toolset.d.ts +7 -1
- package/dist/toolset.js +13 -4
- package/package.json +6 -1
|
@@ -34,3 +34,9 @@ export function entryRows(records, meta, registry, locale) {
|
|
|
34
34
|
};
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
+
/** The list drags only when the order IS the `sort` column and the rows
|
|
38
|
+
* are siblings: a nested collection's flat index is not a sibling index
|
|
39
|
+
* (spec 2026-09-09 §5; nested manual collections wait for the entry tree UI). */
|
|
40
|
+
export function canReorder(meta) {
|
|
41
|
+
return meta.manual && meta.depth === 1;
|
|
42
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PageLocale, PageRecord } from "../../store.ts";
|
|
2
2
|
import type { AdminCollection, AdminPageTemplate, AdminRegistry } from "../serialize.ts";
|
|
3
|
+
import type { Projection } from "../ui/useSortable.ts";
|
|
3
4
|
type NodeLike = {
|
|
4
5
|
parentId: string | null;
|
|
5
6
|
locales: Record<string, {
|
|
@@ -54,4 +55,27 @@ export type PagesTreeRow = {
|
|
|
54
55
|
record: PageRecord;
|
|
55
56
|
};
|
|
56
57
|
export declare function pagesTreeRows(records: PageRecord[], registry: AdminRegistry, locale: string, collapsed: ReadonlySet<string>): PagesTreeRow[];
|
|
58
|
+
export type TreeProjection = Projection;
|
|
59
|
+
/** Every row drags: root order is the listing's order, nothing code owns.
|
|
60
|
+
* Fixed nodes and the home page stay at the root — projectDrop refuses
|
|
61
|
+
* them any deeper slot, the move op refuses any other parent. */
|
|
62
|
+
export declare const canDragRow: (_row: PagesTreeRow) => boolean;
|
|
63
|
+
/** Whether a row may only reorder among the root siblings. */
|
|
64
|
+
export declare const rootOnly: (row: PagesTreeRow) => boolean;
|
|
65
|
+
/** Levels below a node: 0 for a leaf. From the records, because the
|
|
66
|
+
* dragged node's rows are collapsed away during the drag. */
|
|
67
|
+
export declare function subtreeHeight(records: {
|
|
68
|
+
id: string;
|
|
69
|
+
parentId: string | null;
|
|
70
|
+
}[], id: string): number;
|
|
71
|
+
/**
|
|
72
|
+
* Reads a flat drop slot as a tree position. `rows` still hold the dragged
|
|
73
|
+
* row at `from` (its subtree collapsed); `slot` is its index in the
|
|
74
|
+
* resulting flat order; `xDrift` is the pointer's horizontal travel and
|
|
75
|
+
* every `indent` px of it is one level. Returns null for a slot the tree
|
|
76
|
+
* refuses: a parent that cannot have children, a subtree that would
|
|
77
|
+
* pass the depth limit, or any depth but the root for a row that is
|
|
78
|
+
* code-owned or the site root.
|
|
79
|
+
*/
|
|
80
|
+
export declare function projectDrop(rows: PagesTreeRow[], from: number, slot: number, xDrift: number, height: number, pageDepth: number, indent?: number): TreeProjection | null;
|
|
57
81
|
export {};
|
|
@@ -84,7 +84,7 @@ export function pagesTreeRows(records, registry, locale, collapsed) {
|
|
|
84
84
|
template: r.template,
|
|
85
85
|
templateTitle: mount
|
|
86
86
|
? r.template === null ? "Collection" : `Collection · ${templateTitle(r.template)}`
|
|
87
|
-
: r.template === null ? "
|
|
87
|
+
: r.template === null ? "Path" : templateTitle(r.template),
|
|
88
88
|
status: !row || r.template === null ? null : localeStatusOf(row),
|
|
89
89
|
// A child needs its parent in the locale (the parent rule), so an absent node offers none.
|
|
90
90
|
canHaveChildren: present && !mount && !isHome && depth + 1 < registry.pageDepth,
|
|
@@ -97,3 +97,61 @@ export function pagesTreeRows(records, registry, locale, collapsed) {
|
|
|
97
97
|
walk(buildPageTree(records, locale, opts), 0, null);
|
|
98
98
|
return rows;
|
|
99
99
|
}
|
|
100
|
+
/** Every row drags: root order is the listing's order, nothing code owns.
|
|
101
|
+
* Fixed nodes and the home page stay at the root — projectDrop refuses
|
|
102
|
+
* them any deeper slot, the move op refuses any other parent. */
|
|
103
|
+
export const canDragRow = (_row) => true;
|
|
104
|
+
/** Whether a row may only reorder among the root siblings. */
|
|
105
|
+
export const rootOnly = (row) => row.fixed || row.isHome;
|
|
106
|
+
/** Levels below a node: 0 for a leaf. From the records, because the
|
|
107
|
+
* dragged node's rows are collapsed away during the drag. */
|
|
108
|
+
export function subtreeHeight(records, id) {
|
|
109
|
+
const children = records.filter((r) => r.parentId === id);
|
|
110
|
+
if (children.length === 0)
|
|
111
|
+
return 0;
|
|
112
|
+
return 1 + Math.max(...children.map((c) => subtreeHeight(records, c.id)));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Reads a flat drop slot as a tree position. `rows` still hold the dragged
|
|
116
|
+
* row at `from` (its subtree collapsed); `slot` is its index in the
|
|
117
|
+
* resulting flat order; `xDrift` is the pointer's horizontal travel and
|
|
118
|
+
* every `indent` px of it is one level. Returns null for a slot the tree
|
|
119
|
+
* refuses: a parent that cannot have children, a subtree that would
|
|
120
|
+
* pass the depth limit, or any depth but the root for a row that is
|
|
121
|
+
* code-owned or the site root.
|
|
122
|
+
*/
|
|
123
|
+
export function projectDrop(rows, from, slot, xDrift, height, pageDepth, indent = 20) {
|
|
124
|
+
const others = rows.filter((_, i) => i !== from);
|
|
125
|
+
const above = slot > 0 ? others[slot - 1] : null;
|
|
126
|
+
const below = slot < others.length ? others[slot] : null;
|
|
127
|
+
const maxDepth = above ? above.depth + 1 : 0;
|
|
128
|
+
const minDepth = below ? below.depth : 0;
|
|
129
|
+
if (minDepth > maxDepth)
|
|
130
|
+
return null;
|
|
131
|
+
const wanted = rows[from].depth + Math.round(xDrift / indent);
|
|
132
|
+
const depth = Math.max(minDepth, Math.min(maxDepth, wanted));
|
|
133
|
+
if (depth + height >= pageDepth)
|
|
134
|
+
return null;
|
|
135
|
+
if (depth > 0 && rootOnly(rows[from]))
|
|
136
|
+
return null;
|
|
137
|
+
let parent = null;
|
|
138
|
+
if (depth > 0) {
|
|
139
|
+
for (let i = slot - 1; i >= 0; i--) {
|
|
140
|
+
if (others[i].depth === depth - 1) {
|
|
141
|
+
parent = others[i];
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!parent || !parent.canHaveChildren)
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const parentId = parent?.id ?? null;
|
|
149
|
+
// A collapsed parent hides its children: the slot after it is "last child".
|
|
150
|
+
if (parent && parent.hasChildren && !parent.expanded)
|
|
151
|
+
return { depth, parentId, index: undefined };
|
|
152
|
+
let index = 0;
|
|
153
|
+
for (let i = 0; i < slot; i++)
|
|
154
|
+
if (others[i].parentId === parentId)
|
|
155
|
+
index++;
|
|
156
|
+
return { depth, parentId, index };
|
|
157
|
+
}
|
package/dist/admin/tree-ops.d.ts
CHANGED
|
@@ -6,7 +6,14 @@ export declare function findNode(tree: PageTree, id: string): TreeNode | null;
|
|
|
6
6
|
export declare function insertNode(tree: PageTree, zone: string, index: number, node: TreeNode): PageTree;
|
|
7
7
|
export declare function removeNode(tree: PageTree, id: string): PageTree;
|
|
8
8
|
export declare function moveNode(tree: PageTree, id: string, dir: -1 | 1): PageTree;
|
|
9
|
+
/** Places `id` at `index` in its zone's RESULTING order (removed first,
|
|
10
|
+
* then spliced in — moveItem's convention); clamped, and the same tree
|
|
11
|
+
* when nothing changes. The drop path; `moveNode` stays for the arrows. */
|
|
12
|
+
export declare function moveNodeTo(tree: PageTree, id: string, index: number): PageTree;
|
|
9
13
|
export declare function patchNode(tree: PageTree, id: string, ns: "fields" | "styles" | "meta", key: string, value: unknown): PageTree;
|
|
14
|
+
/** patchNode for a dotted path inside `fields` (spec 2026-09-10 §1):
|
|
15
|
+
* `points.2.heading` sets one item's heading and copies only the spine. */
|
|
16
|
+
export declare function patchNodePath(tree: PageTree, id: string, path: string, value: unknown): PageTree;
|
|
10
17
|
export declare function canInsert(policy: ZonePolicy | undefined, count: number): boolean;
|
|
11
18
|
export declare function allowedSections(policy: ZonePolicy | undefined, all: AdminSection[]): AdminSection[];
|
|
12
19
|
/** The visual items the picker offers in a zone: materialized (an id is
|
|
@@ -15,7 +22,8 @@ export declare function allowedShared(policy: ZonePolicy | undefined, shared: Ad
|
|
|
15
22
|
/** A page places a visual item as this node (spec 2026-09-07 §6): `item`
|
|
16
23
|
* names the registration, `ref` is the item's row id. */
|
|
17
24
|
export declare function placementNode(item: AdminShared, ref: string, id: string): TreeNode;
|
|
18
|
-
|
|
25
|
+
/** Also fills an element node (an `AdminElement` is a section shape plus contexts). */
|
|
26
|
+
export declare function nodeFromSample(section: Pick<AdminSection, "name" | "fields" | "styles" | "sample">, id: string): TreeNode;
|
|
19
27
|
/** A locked zone holds exactly one node the editor can never insert or
|
|
20
28
|
* remove — so someone has to put it there. This does, from the component's
|
|
21
29
|
* sample (or, for a zone bound to a shared item, a placement), for every
|
package/dist/admin/tree-ops.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SHARED_NODE_TYPE } from "../shared.js";
|
|
2
|
+
import { setAtPath } from "./field-paths.js";
|
|
2
3
|
export function zoneOf(tree, id) {
|
|
3
4
|
for (const [zone, nodes] of Object.entries(tree.zones)) {
|
|
4
5
|
if (nodes.some((n) => n.id === id))
|
|
@@ -40,12 +41,37 @@ export function moveNode(tree, id, dir) {
|
|
|
40
41
|
[list[i], list[j]] = [list[j], list[i]];
|
|
41
42
|
return withZone(tree, zone, list);
|
|
42
43
|
}
|
|
44
|
+
/** Places `id` at `index` in its zone's RESULTING order (removed first,
|
|
45
|
+
* then spliced in — moveItem's convention); clamped, and the same tree
|
|
46
|
+
* when nothing changes. The drop path; `moveNode` stays for the arrows. */
|
|
47
|
+
export function moveNodeTo(tree, id, index) {
|
|
48
|
+
const zone = zoneOf(tree, id);
|
|
49
|
+
if (!zone)
|
|
50
|
+
return tree;
|
|
51
|
+
const list = tree.zones[zone];
|
|
52
|
+
const from = list.findIndex((n) => n.id === id);
|
|
53
|
+
const to = Math.max(0, Math.min(index, list.length - 1));
|
|
54
|
+
if (from === to)
|
|
55
|
+
return tree;
|
|
56
|
+
const next = [...list];
|
|
57
|
+
const [moved] = next.splice(from, 1);
|
|
58
|
+
next.splice(to, 0, moved);
|
|
59
|
+
return withZone(tree, zone, next);
|
|
60
|
+
}
|
|
43
61
|
export function patchNode(tree, id, ns, key, value) {
|
|
44
62
|
const zone = zoneOf(tree, id);
|
|
45
63
|
if (!zone)
|
|
46
64
|
return tree;
|
|
47
65
|
return withZone(tree, zone, tree.zones[zone].map((n) => (n.id === id ? { ...n, [ns]: { ...(n[ns] ?? {}), [key]: value } } : n)));
|
|
48
66
|
}
|
|
67
|
+
/** patchNode for a dotted path inside `fields` (spec 2026-09-10 §1):
|
|
68
|
+
* `points.2.heading` sets one item's heading and copies only the spine. */
|
|
69
|
+
export function patchNodePath(tree, id, path, value) {
|
|
70
|
+
const zone = zoneOf(tree, id);
|
|
71
|
+
if (!zone)
|
|
72
|
+
return tree;
|
|
73
|
+
return withZone(tree, zone, tree.zones[zone].map((n) => (n.id === id ? { ...n, fields: setAtPath(n.fields, path, value) } : n)));
|
|
74
|
+
}
|
|
49
75
|
export function canInsert(policy, count) {
|
|
50
76
|
if (!policy || policy.kind === "locked")
|
|
51
77
|
return false;
|
|
@@ -85,6 +111,7 @@ const EMPTY_BY_TYPE = { refList: [], object: {} };
|
|
|
85
111
|
* broken-image icon or a stray zero, worse than nothing at all. With no
|
|
86
112
|
* sample, leave the key out of `fields` entirely rather than seed a lie. */
|
|
87
113
|
const ABSENT_WHEN_UNSAMPLED = new Set(["image", "video", "file", "number"]);
|
|
114
|
+
/** Also fills an element node (an `AdminElement` is a section shape plus contexts). */
|
|
88
115
|
export function nodeFromSample(section, id) {
|
|
89
116
|
const fields = {};
|
|
90
117
|
const styles = {};
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { AdminRegistry } from "../serialize.ts";
|
|
2
|
+
/** Above this many supported locales the switch is a dropdown. */
|
|
3
|
+
export declare const SEGMENTED_MAX = 3;
|
|
4
|
+
/** "fi" → "Fi": the locale code as the pill reads it. */
|
|
5
|
+
export declare const localeLabel: (locale: string) => string;
|
|
2
6
|
export declare function LocaleSwitcher({ registry, present, parentLocales, value, adding, onSwitch, onAdd }: {
|
|
3
7
|
registry: AdminRegistry;
|
|
4
8
|
/** The locales the record has; `[value]` while nothing is loaded. */
|
|
@@ -9,3 +13,9 @@ export declare function LocaleSwitcher({ registry, present, parentLocales, value
|
|
|
9
13
|
onSwitch(locale: string): void;
|
|
10
14
|
onAdd(locale: string): void;
|
|
11
15
|
}): import("react").JSX.Element;
|
|
16
|
+
/** The lists' locale selector: every locale exists, nothing to add. */
|
|
17
|
+
export declare function LocalePicker({ locales, value, onChange }: {
|
|
18
|
+
locales: readonly string[];
|
|
19
|
+
value: string;
|
|
20
|
+
onChange(locale: string): void;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Segmented } from "./primitives.js";
|
|
4
|
+
/** Above this many supported locales the switch is a dropdown. */
|
|
5
|
+
export const SEGMENTED_MAX = 3;
|
|
6
|
+
/** "fi" → "Fi": the locale code as the pill reads it. */
|
|
7
|
+
export const localeLabel = (locale) => locale.charAt(0).toUpperCase() + locale.slice(1);
|
|
3
8
|
export function LocaleSwitcher({ registry, present, parentLocales, value, adding, onSwitch, onAdd }) {
|
|
4
|
-
const
|
|
9
|
+
const supported = registry.locales.supported;
|
|
10
|
+
const missing = supported.filter((l) => !present.includes(l) && l !== value);
|
|
11
|
+
const blocked = (l) => !parentLocales.includes(l);
|
|
12
|
+
const reason = (l) => `add ${l} to the parent first`;
|
|
13
|
+
if (supported.length <= SEGMENTED_MAX) {
|
|
14
|
+
return (_jsx(Segmented, { options: supported, value: value, disabled: adding, label: localeLabel, className: "sm-seg--locale", option: (l) => {
|
|
15
|
+
if (!missing.includes(l))
|
|
16
|
+
return undefined;
|
|
17
|
+
return blocked(l)
|
|
18
|
+
? { disabled: true, title: reason(l), className: "sm-seg__opt--absent" }
|
|
19
|
+
: { title: `Not in ${l} yet`, className: "sm-seg__opt--absent" };
|
|
20
|
+
}, onChange: onSwitch }));
|
|
21
|
+
}
|
|
5
22
|
return (_jsxs("select", { className: "sm-input", style: { width: "auto" }, value: value, "aria-label": "Locale", onChange: (e) => { const v = e.target.value; if (v.startsWith("+"))
|
|
6
23
|
onAdd(v.slice(1));
|
|
7
24
|
else
|
|
8
|
-
onSwitch(v); }, children: [present.map((l) => _jsx("option", { value: l, children: l }, l)), !present.includes(value) && _jsxs("option", { value: value, children: [value, " (not yet)"] }), missing.length > 0 && (_jsx("optgroup", { label: "Add locale", disabled: adding, children: missing.map((l) =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
onSwitch(v); }, children: [present.map((l) => _jsx("option", { value: l, children: localeLabel(l) }, l)), !present.includes(value) && _jsxs("option", { value: value, children: [localeLabel(value), " (not yet)"] }), missing.length > 0 && (_jsx("optgroup", { label: "Add locale", disabled: adding, children: missing.map((l) => blocked(l)
|
|
26
|
+
? _jsxs("option", { value: `+${l}`, disabled: true, title: reason(l), children: ["+ ", localeLabel(l), " \u2014 ", reason(l)] }, l)
|
|
27
|
+
: _jsxs("option", { value: `+${l}`, children: ["+ ", localeLabel(l)] }, l)) }))] }));
|
|
28
|
+
}
|
|
29
|
+
/** The lists' locale selector: every locale exists, nothing to add. */
|
|
30
|
+
export function LocalePicker({ locales, value, onChange }) {
|
|
31
|
+
if (locales.length <= SEGMENTED_MAX) {
|
|
32
|
+
return _jsx(Segmented, { options: locales, value: value, onChange: onChange, label: localeLabel, className: "sm-seg--locale" });
|
|
33
|
+
}
|
|
34
|
+
return (_jsx("select", { className: "sm-input", style: { width: "auto" }, value: value, "aria-label": "Locale", onChange: (e) => onChange(e.target.value), children: locales.map((l) => _jsx("option", { value: l, children: localeLabel(l) }, l)) }));
|
|
15
35
|
}
|
package/dist/admin/ui/Modal.js
CHANGED
|
@@ -2,14 +2,26 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
// The one modal: a fixed overlay (click outside or Escape closes) and a
|
|
4
4
|
// centred panel with a title row. Same overlay shape as InsertPicker.
|
|
5
|
-
|
|
5
|
+
// Rendered through a portal into the admin wrapper: a picker opened from
|
|
6
|
+
// a card inside the canvas iframe must not be fixed to the iframe's
|
|
7
|
+
// viewport (spec 2026-09-10 §5), and every modal escapes its parent's
|
|
8
|
+
// overflow the same way. Static rendering (no document) keeps it inline.
|
|
9
|
+
import { useEffect, useState } from "react";
|
|
10
|
+
import { createPortal } from "react-dom";
|
|
6
11
|
import { IconButton } from "./primitives.js";
|
|
12
|
+
function adminRoot() {
|
|
13
|
+
if (typeof document === "undefined")
|
|
14
|
+
return null;
|
|
15
|
+
return document.querySelector(".sm-admin") ?? document.body;
|
|
16
|
+
}
|
|
7
17
|
export function Modal({ title, width = 720, onClose, children, footer }) {
|
|
18
|
+
const [root] = useState(adminRoot);
|
|
8
19
|
useEffect(() => {
|
|
9
20
|
const onKey = (e) => { if (e.key === "Escape")
|
|
10
21
|
onClose(); };
|
|
11
22
|
window.addEventListener("keydown", onKey);
|
|
12
23
|
return () => window.removeEventListener("keydown", onKey);
|
|
13
24
|
}, [onClose]);
|
|
14
|
-
|
|
25
|
+
const modal = (_jsx("div", { className: "sm-modal", onClick: onClose, children: _jsxs("div", { className: "sm-modal__panel", role: "dialog", "aria-label": title, style: { width }, onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "sm-modal__head", children: [_jsx("span", { className: "sm-title", children: title }), _jsx(IconButton, { label: "Close", onClick: onClose, children: "\u00D7" })] }), _jsx("div", { className: "sm-modal__body", children: children }), footer && _jsx("div", { className: "sm-modal__foot", children: footer })] }) }));
|
|
26
|
+
return root ? createPortal(modal, root) : modal;
|
|
15
27
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ResizeHandle(): import("react").JSX.Element;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
// The inspector's drag-to-resize handle (spec 2026-09-10 §3–§5). Rendered
|
|
4
|
+
// between the main column and <aside class="sm-inspector"> inside .sm-editor.
|
|
5
|
+
// A drag writes --inspector-w straight onto the .sm-admin wrapper — no React
|
|
6
|
+
// state, so the canvas iframe is never re-rendered mid-gesture; only the
|
|
7
|
+
// aria-valuenow re-renders, on release and after a keyboard step.
|
|
8
|
+
import { useEffect, useRef, useState } from "react";
|
|
9
|
+
import { INSPECTOR_W_KEY, MIN_INSPECTOR_W, RESIZE_STEP, clampInspectorW, maxInspectorW } from "./resize.js";
|
|
10
|
+
/** The wrapper the variable lives on, the grid row, and the inspector itself. */
|
|
11
|
+
const parts = (handle) => {
|
|
12
|
+
const admin = handle.closest(".sm-admin");
|
|
13
|
+
const grid = handle.parentElement;
|
|
14
|
+
const aside = handle.nextElementSibling;
|
|
15
|
+
return {
|
|
16
|
+
admin: admin instanceof HTMLElement ? admin : null,
|
|
17
|
+
grid: grid instanceof HTMLElement ? grid : null,
|
|
18
|
+
aside: aside instanceof HTMLElement ? aside : null,
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
const apply = (admin, px) => admin.style.setProperty("--inspector-w", `${px}px`);
|
|
22
|
+
/** Private mode and blocked site data both throw; the width still changes. */
|
|
23
|
+
const remember = (px) => {
|
|
24
|
+
try {
|
|
25
|
+
if (px === null)
|
|
26
|
+
localStorage.removeItem(INSPECTOR_W_KEY);
|
|
27
|
+
else
|
|
28
|
+
localStorage.setItem(INSPECTOR_W_KEY, String(px));
|
|
29
|
+
}
|
|
30
|
+
catch { /* not remembered */ }
|
|
31
|
+
};
|
|
32
|
+
export function ResizeHandle() {
|
|
33
|
+
const ref = useRef(null);
|
|
34
|
+
const drag = useRef(null);
|
|
35
|
+
const [now, setNow] = useState(null);
|
|
36
|
+
// A hidden tab lays out at zero width (seen in the desktop pane's walk):
|
|
37
|
+
// zero is "unknown", not a value — measured again on focus.
|
|
38
|
+
const measure = () => {
|
|
39
|
+
const el = ref.current;
|
|
40
|
+
if (!el)
|
|
41
|
+
return;
|
|
42
|
+
const { aside } = parts(el);
|
|
43
|
+
if (aside && aside.offsetWidth > 0)
|
|
44
|
+
setNow(aside.offsetWidth);
|
|
45
|
+
};
|
|
46
|
+
useEffect(measure, []);
|
|
47
|
+
/** Clamp, write and remember one width; the aria value follows. */
|
|
48
|
+
const commit = (px) => {
|
|
49
|
+
const el = ref.current;
|
|
50
|
+
if (!el)
|
|
51
|
+
return;
|
|
52
|
+
const { admin, grid } = parts(el);
|
|
53
|
+
if (!admin || !grid)
|
|
54
|
+
return;
|
|
55
|
+
const next = clampInspectorW(px, grid.clientWidth);
|
|
56
|
+
apply(admin, next);
|
|
57
|
+
remember(next);
|
|
58
|
+
setNow(next);
|
|
59
|
+
};
|
|
60
|
+
const reset = () => {
|
|
61
|
+
const el = ref.current;
|
|
62
|
+
if (!el)
|
|
63
|
+
return;
|
|
64
|
+
const { admin } = parts(el);
|
|
65
|
+
if (!admin)
|
|
66
|
+
return;
|
|
67
|
+
admin.style.removeProperty("--inspector-w");
|
|
68
|
+
remember(null);
|
|
69
|
+
measure();
|
|
70
|
+
};
|
|
71
|
+
const onPointerDown = (e) => {
|
|
72
|
+
if (e.button !== 0 || drag.current)
|
|
73
|
+
return;
|
|
74
|
+
const { admin, grid, aside } = parts(e.currentTarget);
|
|
75
|
+
if (!admin || !grid || !aside)
|
|
76
|
+
return;
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
79
|
+
drag.current = { id: e.pointerId, startX: e.clientX, startW: aside.offsetWidth, rowW: grid.clientWidth, last: aside.offsetWidth };
|
|
80
|
+
admin.classList.add("sm-admin--resizing");
|
|
81
|
+
};
|
|
82
|
+
const onPointerMove = (e) => {
|
|
83
|
+
const d = drag.current;
|
|
84
|
+
if (!d || e.pointerId !== d.id)
|
|
85
|
+
return;
|
|
86
|
+
const { admin } = parts(e.currentTarget);
|
|
87
|
+
if (!admin)
|
|
88
|
+
return;
|
|
89
|
+
// The handle sits on the inspector's left edge: moving left widens it.
|
|
90
|
+
d.last = clampInspectorW(d.startW - (e.clientX - d.startX), d.rowW);
|
|
91
|
+
apply(admin, d.last);
|
|
92
|
+
};
|
|
93
|
+
// Also fires from releasePointerCapture below, when drag.current is already null.
|
|
94
|
+
const finish = (e) => {
|
|
95
|
+
const d = drag.current;
|
|
96
|
+
if (!d || e.pointerId !== d.id)
|
|
97
|
+
return;
|
|
98
|
+
drag.current = null;
|
|
99
|
+
const { admin } = parts(e.currentTarget);
|
|
100
|
+
if (e.currentTarget.hasPointerCapture(d.id))
|
|
101
|
+
e.currentTarget.releasePointerCapture(d.id);
|
|
102
|
+
admin?.classList.remove("sm-admin--resizing");
|
|
103
|
+
remember(d.last);
|
|
104
|
+
setNow(d.last);
|
|
105
|
+
};
|
|
106
|
+
const onKeyDown = (e) => {
|
|
107
|
+
const { grid, aside } = parts(e.currentTarget);
|
|
108
|
+
if (!grid || !aside || drag.current)
|
|
109
|
+
return;
|
|
110
|
+
const w = aside.offsetWidth;
|
|
111
|
+
let next = null;
|
|
112
|
+
if (e.key === "ArrowLeft")
|
|
113
|
+
next = w + RESIZE_STEP;
|
|
114
|
+
else if (e.key === "ArrowRight")
|
|
115
|
+
next = w - RESIZE_STEP;
|
|
116
|
+
else if (e.key === "Home")
|
|
117
|
+
next = MIN_INSPECTOR_W;
|
|
118
|
+
else if (e.key === "End")
|
|
119
|
+
next = maxInspectorW(grid.clientWidth);
|
|
120
|
+
if (next === null)
|
|
121
|
+
return;
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
e.stopPropagation(); // the editor's window keydown must not see these
|
|
124
|
+
commit(next);
|
|
125
|
+
};
|
|
126
|
+
return (_jsx("div", { ref: ref, className: "sm-resize", role: "separator", "aria-orientation": "vertical", "aria-label": "Resize inspector", "aria-valuemin": MIN_INSPECTOR_W, "aria-valuenow": now ?? undefined, title: "Drag to resize \u00B7 double-click to reset", tabIndex: 0, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: finish, onPointerCancel: finish, onLostPointerCapture: finish, onDoubleClick: reset, onKeyDown: onKeyDown, onFocus: measure }));
|
|
127
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, CSSProperties, ReactNode } from "react";
|
|
2
2
|
import type { SaveState } from "../editor/autosave.ts";
|
|
3
3
|
/** The wordmark, inlined: a package can't ship a file into the host app's
|
|
4
|
-
* /public.
|
|
4
|
+
* /public. Its colour is the --logo-color token (accent in the warm look). */
|
|
5
5
|
export declare function Logo({ height }: {
|
|
6
6
|
height?: number;
|
|
7
7
|
}): import("react").JSX.Element;
|
|
@@ -27,11 +27,17 @@ export declare function IconButton({ label, tone, size, children, ...rest }: {
|
|
|
27
27
|
size?: number;
|
|
28
28
|
children: ReactNode;
|
|
29
29
|
} & ButtonHTMLAttributes<HTMLButtonElement>): import("react").JSX.Element;
|
|
30
|
+
/** The drag handle every sortable row carries at its left edge (spec
|
|
31
|
+
* 2026-09-09 §2): a button, so it is focusable and takes the arrow keys;
|
|
32
|
+
* `touch-action: none` (theme) so a finger on it drags instead of scrolling. */
|
|
33
|
+
export declare function Grip(props: ButtonHTMLAttributes<HTMLButtonElement>): import("react").JSX.Element;
|
|
30
34
|
/** Label + optional counter above a control — the handoff's Input/Textarea head.
|
|
31
35
|
* `asDiv` for groups holding more than one control: an implicit <label>
|
|
32
36
|
* activates its first labelable descendant, so wrapping several is a trap. */
|
|
33
|
-
export declare function Field({ label, counter, hint, error, asDiv, children, style, className }: {
|
|
37
|
+
export declare function Field({ label, mark, counter, hint, error, asDiv, children, style, className, path }: {
|
|
34
38
|
label?: ReactNode;
|
|
39
|
+
/** A small glyph after the label — the field's "same in every language" mark. */
|
|
40
|
+
mark?: ReactNode;
|
|
35
41
|
counter?: ReactNode;
|
|
36
42
|
hint?: ReactNode;
|
|
37
43
|
error?: ReactNode;
|
|
@@ -39,17 +45,27 @@ export declare function Field({ label, counter, hint, error, asDiv, children, st
|
|
|
39
45
|
children: ReactNode;
|
|
40
46
|
style?: CSSProperties;
|
|
41
47
|
className?: string;
|
|
48
|
+
/** The field's dotted path, for the canvas reveal (spec 2026-09-10 §8). */
|
|
49
|
+
path?: string;
|
|
42
50
|
}): import("react").JSX.Element;
|
|
51
|
+
/** The mark beside a field that is NOT `.localized()`: a globe-off, so a
|
|
52
|
+
* multilingual form needs no "shared across languages" grouping. */
|
|
53
|
+
export declare function UnlocalizedMark(): import("react").JSX.Element;
|
|
43
54
|
export declare function TopBar({ left, right }: {
|
|
44
55
|
left: ReactNode;
|
|
45
56
|
right?: ReactNode;
|
|
46
57
|
}): import("react").JSX.Element;
|
|
47
58
|
/** `items` are crumbs; every one but the last is a link when given an href.
|
|
48
|
-
* Links route in place: the shell never reloads for a crumb.
|
|
59
|
+
* Links route in place: the shell never reloads for a crumb. A crumb whose
|
|
60
|
+
* label starts with "/" (a page path) brings its own slash, so no
|
|
61
|
+
* separator is drawn before it. A crumb's `trailing` node renders right
|
|
62
|
+
* after it — the locale switch rides the first crumb, so it keeps its
|
|
63
|
+
* place however long the title or path after it grows. */
|
|
49
64
|
export declare function Breadcrumb({ items, backHref }: {
|
|
50
65
|
items: {
|
|
51
66
|
label: string;
|
|
52
67
|
href?: string;
|
|
68
|
+
trailing?: ReactNode;
|
|
53
69
|
}[];
|
|
54
70
|
backHref?: string;
|
|
55
71
|
}): import("react").JSX.Element;
|
|
@@ -58,12 +74,21 @@ export declare function Tabs<T extends string>({ tabs, value, onChange }: {
|
|
|
58
74
|
value: T | undefined;
|
|
59
75
|
onChange(tab: T): void;
|
|
60
76
|
}): import("react").JSX.Element;
|
|
61
|
-
export declare function Segmented<T extends string>({ options, value, onChange, stretch, disabled }: {
|
|
77
|
+
export declare function Segmented<T extends string>({ options, value, onChange, stretch, disabled, label, option, className }: {
|
|
62
78
|
options: readonly T[];
|
|
63
79
|
value: T;
|
|
64
80
|
onChange(v: T): void;
|
|
65
81
|
stretch?: boolean;
|
|
66
82
|
disabled?: boolean;
|
|
83
|
+
className?: string;
|
|
84
|
+
/** What an option reads as; the value itself by default. */
|
|
85
|
+
label?(o: T): ReactNode;
|
|
86
|
+
/** Per-option state: disabled with a reason, or a modifier class (an absent locale). */
|
|
87
|
+
option?(o: T): {
|
|
88
|
+
disabled?: boolean;
|
|
89
|
+
title?: string;
|
|
90
|
+
className?: string;
|
|
91
|
+
} | undefined;
|
|
67
92
|
}): import("react").JSX.Element;
|
|
68
93
|
export declare function StatusBadge({ status }: {
|
|
69
94
|
status: string;
|
|
@@ -5,16 +5,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
// values live here; every static rule, and all hover/focus state, lives in
|
|
6
6
|
// theme.tsx's stylesheet.
|
|
7
7
|
import { Fragment } from "react";
|
|
8
|
-
import { Moon, Sun } from "lucide-react";
|
|
8
|
+
import { GlobeOff, GripVertical, Moon, Sun } from "lucide-react";
|
|
9
9
|
import { THEME_KEY } from "./theme.js";
|
|
10
10
|
import { RouteLink } from "../shell/RouteLink.js";
|
|
11
11
|
/** Lucide at the design system's size and stroke. */
|
|
12
12
|
const ICON = { size: 14, strokeWidth: 1.5, "aria-hidden": true };
|
|
13
13
|
/* ── brand ───────────────────────────────────────────────────────────── */
|
|
14
14
|
/** The wordmark, inlined: a package can't ship a file into the host app's
|
|
15
|
-
* /public.
|
|
15
|
+
* /public. Its colour is the --logo-color token (accent in the warm look). */
|
|
16
16
|
export function Logo({ height = 18 }) {
|
|
17
|
-
return (_jsx("svg", { className: "sm-wordmark", style: { height }, viewBox: "0 0 183 43", fill: "none", xmlns: "http://www.w3.org/2000/svg", role: "img", "aria-label": "Smoodly", children: _jsx("path", { fill: "
|
|
17
|
+
return (_jsx("svg", { className: "sm-wordmark", style: { height }, viewBox: "0 0 183 43", fill: "none", xmlns: "http://www.w3.org/2000/svg", role: "img", "aria-label": "Smoodly", children: _jsx("path", { fill: "currentColor", d: "M13.294 9.9796C15.9908 9.9796 19.823 10.7144 21.4552 11.5204L20.1069 18.3473C18.1198 17.4228 15.3284 16.688 12.7973 16.688C11.307 16.688 10.3371 17.0198 10.3371 17.8021C10.3371 18.3236 10.8575 18.6318 11.7801 18.8214L13.7198 19.2481C17.7413 20.1251 19.7993 22.2111 19.7993 25.4112C19.7993 31.9063 14.7607 34.3716 8.23173 34.3716C4.9909 34.3716 1.8447 33.5893 0.046875 32.8782L1.48987 25.6009C3.90274 26.8572 7.14357 27.6395 9.22526 27.6395C10.7156 27.6395 11.7328 27.2602 11.7328 26.3357C11.7328 25.8142 11.3543 25.4112 10.2425 25.2216L8.58656 24.8897C5.44036 24.2971 2.74362 22.14 2.74362 18.6792C2.74362 13.2982 6.81239 10.0033 13.3413 10.0033L13.294 9.9796ZM52.8463 9.9796C49.014 9.9796 46.6012 12.0656 45.2528 13.5116C44.1883 11.3308 41.9174 9.9796 39.2916 9.9796C36.6658 9.9796 34.6551 10.9989 33.283 12.3975L33.6142 10.7381H26.0681L21.6208 33.6604H29.2616L32.0766 19.011C32.9992 17.8495 34.2529 16.9724 35.6013 16.9724C37.4464 16.9724 38.2034 18.2288 37.8249 20.22L35.2228 33.6604H42.8636L45.6786 18.9162C46.6012 17.8021 47.8549 16.9724 49.156 16.9724C51.0011 16.9724 51.7344 18.2288 51.3796 20.22L49.9366 27.7817C49.5581 29.7255 49.4635 31.9537 50.1732 33.6604H57.8139C57.1279 31.9537 57.1989 29.7255 57.5774 27.7817L59.3752 18.3236C60.3451 13.3219 57.1043 10.027 52.8463 10.027V9.9796ZM109.596 19.6273C109.596 27.6158 103.304 34.3241 94.7877 34.3241C89.7727 34.3241 86.1061 31.9537 84.6158 28.3743C84.4975 28.1136 84.19 28.0662 84.0244 28.3032C81.3986 31.93 77.0933 34.3241 72.031 34.3241C65.4074 34.3241 61.0548 30.1996 61.0548 24.629C61.0548 16.688 67.2998 9.9796 75.8159 9.9796C80.8545 9.9796 84.5211 12.3501 86.0351 15.882C86.1534 16.1428 86.4609 16.1902 86.6265 15.9531C89.2523 12.3501 93.5103 9.9559 98.5963 9.9559C105.267 9.9559 109.62 14.0805 109.62 19.6036L109.596 19.6273ZM78.7965 20.9785C78.7965 18.7029 77.2825 17.1858 74.8697 17.1858C71.6761 17.1858 69.0267 19.9592 69.0267 23.349C69.0267 25.672 70.5643 27.1417 72.9062 27.1417C76.0997 27.1417 78.7728 24.3682 78.7728 20.9785H78.7965ZM101.577 20.9785C101.577 18.7029 100.039 17.1858 97.65 17.1858C94.4565 17.1858 91.8307 19.9592 91.8307 23.349C91.8307 25.672 93.3447 27.1417 95.7103 27.1417C98.9038 27.1417 101.601 24.3682 101.601 20.9785H101.577ZM134.529 33.6367H126.888C126.652 33.1152 126.51 32.5226 126.463 31.93C124.972 33.0915 122.299 34.3479 119.484 34.3479C114.02 34.3479 110.968 29.9388 110.921 24.7475C110.921 16.7591 116.527 10.0033 123.837 10.0033C126.108 10.0033 128.331 10.6433 129.68 12.0419L132.022 0H139.662L134.293 27.7343C133.914 29.6781 133.819 31.9063 134.529 33.6367ZM128.331 18.9873C127.503 17.8258 126.06 17.1858 124.57 17.1858C121.14 17.1858 118.964 20.0066 118.964 23.1356C118.822 25.6957 120.454 27.1654 122.749 27.1654C124.002 27.1654 125.848 26.4305 127.101 25.3638L128.308 19.011L128.331 18.9873ZM146.002 27.758L151.348 0H143.708L138.338 27.7343C137.959 29.6781 137.865 31.9063 138.574 33.613H146.215C145.529 31.9063 145.624 29.6781 145.978 27.7343L146.002 27.758ZM169.752 10.7381L162.064 23.823L158.776 10.7381H150.71L156.86 32.4041L150.095 43.0237H158.421L178.552 10.7381H169.705H169.752ZM179.167 24.8423C176.258 24.8423 173.608 27.4024 173.608 30.2707C173.608 32.3093 175.099 33.8975 177.417 33.8975C180.421 33.8975 183.023 31.3374 183.023 28.4691C183.023 26.3831 181.486 24.8423 179.191 24.8423H179.167Z" }) }));
|
|
18
18
|
}
|
|
19
19
|
/** Light ⇄ dark, remembered in localStorage; an entry in the rail's user menu.
|
|
20
20
|
* The attribute goes on the ADMIN wrapper, never the host <html>: a customer's
|
|
@@ -51,34 +51,53 @@ export function LinkButton({ variant = "secondary", children, ...rest }) {
|
|
|
51
51
|
export function IconButton({ label, tone = "default", size = 24, children, ...rest }) {
|
|
52
52
|
return (_jsx("button", { type: "button", "aria-label": label, title: label, className: cx("sm-iconbtn", tone === "inverse" && "sm-iconbtn--inverse"), style: { width: size, height: size }, ...rest, children: children }));
|
|
53
53
|
}
|
|
54
|
+
/** The drag handle every sortable row carries at its left edge (spec
|
|
55
|
+
* 2026-09-09 §2): a button, so it is focusable and takes the arrow keys;
|
|
56
|
+
* `touch-action: none` (theme) so a finger on it drags instead of scrolling. */
|
|
57
|
+
export function Grip(props) {
|
|
58
|
+
return (_jsx("button", { type: "button", className: "sm-grip", "aria-label": "Drag to reorder", title: "Drag to reorder", ...props, children: _jsx(GripVertical, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) }));
|
|
59
|
+
}
|
|
54
60
|
/** Label + optional counter above a control — the handoff's Input/Textarea head.
|
|
55
61
|
* `asDiv` for groups holding more than one control: an implicit <label>
|
|
56
62
|
* activates its first labelable descendant, so wrapping several is a trap. */
|
|
57
|
-
export function Field({ label, counter, hint, error, asDiv, children, style, className }) {
|
|
63
|
+
export function Field({ label, mark, counter, hint, error, asDiv, children, style, className, path }) {
|
|
58
64
|
const Tag = asDiv ? "div" : "label";
|
|
59
|
-
return (_jsxs(Tag, { className: cx("sm-field", className), style: style, children: [(label || counter) && (_jsxs("span", { className: "sm-field__head", children: [
|
|
65
|
+
return (_jsxs(Tag, { className: cx("sm-field", className), style: style, "data-sm-path": path, children: [(label || counter) && (_jsxs("span", { className: "sm-field__head", children: [_jsxs("span", { className: "sm-label", children: [label, mark] }), counter && _jsx("span", { className: "sm-counter", children: counter })] })), children, hint && _jsx("span", { className: "sm-hint", children: hint }), error && _jsx("span", { className: "sm-error", children: error })] }));
|
|
66
|
+
}
|
|
67
|
+
/** The mark beside a field that is NOT `.localized()`: a globe-off, so a
|
|
68
|
+
* multilingual form needs no "shared across languages" grouping. */
|
|
69
|
+
export function UnlocalizedMark() {
|
|
70
|
+
const title = "Same in every language";
|
|
71
|
+
return (_jsx("span", { className: "sm-label__mark", role: "img", "aria-label": title, title: title, children: _jsx(GlobeOff, { size: 13, strokeWidth: 1.75, "aria-hidden": "true" }) }));
|
|
60
72
|
}
|
|
61
73
|
/* ── navigation ──────────────────────────────────────────────────────── */
|
|
62
74
|
export function TopBar({ left, right }) {
|
|
63
75
|
return (_jsxs("header", { className: "sm-topbar", children: [_jsx("div", { className: "sm-topbar__left", children: left }), _jsx("div", { className: "sm-topbar__right", children: right })] }));
|
|
64
76
|
}
|
|
65
77
|
/** `items` are crumbs; every one but the last is a link when given an href.
|
|
66
|
-
* Links route in place: the shell never reloads for a crumb.
|
|
78
|
+
* Links route in place: the shell never reloads for a crumb. A crumb whose
|
|
79
|
+
* label starts with "/" (a page path) brings its own slash, so no
|
|
80
|
+
* separator is drawn before it. A crumb's `trailing` node renders right
|
|
81
|
+
* after it — the locale switch rides the first crumb, so it keeps its
|
|
82
|
+
* place however long the title or path after it grows. */
|
|
67
83
|
export function Breadcrumb({ items, backHref }) {
|
|
68
84
|
return (_jsxs("div", { className: "sm-crumbs", children: [backHref && _jsx(RouteLink, { className: "sm-back", href: backHref, "aria-label": "Back", children: "\u2190" }), items.map((it, i) => {
|
|
69
85
|
const last = i === items.length - 1;
|
|
70
|
-
return (_jsxs(Fragment, { children: [i > 0 && _jsx("span", { className: "sm-crumbs__sep", children: "/" }), last
|
|
86
|
+
return (_jsxs(Fragment, { children: [i > 0 && !it.label.startsWith("/") && _jsx("span", { className: "sm-crumbs__sep", children: "/" }), last
|
|
71
87
|
? _jsx("span", { className: "sm-crumbs__here", children: it.label })
|
|
72
88
|
: it.href
|
|
73
89
|
? _jsx(RouteLink, { href: it.href, style: { color: "inherit" }, children: it.label })
|
|
74
|
-
: _jsx("span", { children: it.label })] }, i));
|
|
90
|
+
: _jsx("span", { children: it.label }), it.trailing] }, i));
|
|
75
91
|
})] }));
|
|
76
92
|
}
|
|
77
93
|
export function Tabs({ tabs, value, onChange }) {
|
|
78
94
|
return (_jsx("div", { className: "sm-tabs", role: "tablist", children: tabs.map((t) => (_jsx("button", { type: "button", role: "tab", "aria-selected": t === value, className: "sm-tab", onClick: () => onChange(t), children: t }, t))) }));
|
|
79
95
|
}
|
|
80
|
-
export function Segmented({ options, value, onChange, stretch, disabled }) {
|
|
81
|
-
return (_jsx("div", { className: cx("sm-seg", stretch && "sm-seg--stretch", disabled && "sm-seg--disabled"), children: options.map((o) =>
|
|
96
|
+
export function Segmented({ options, value, onChange, stretch, disabled, label, option, className }) {
|
|
97
|
+
return (_jsx("div", { className: cx("sm-seg", stretch && "sm-seg--stretch", disabled && "sm-seg--disabled", className), children: options.map((o) => {
|
|
98
|
+
const extra = option?.(o);
|
|
99
|
+
return (_jsx("button", { type: "button", className: cx("sm-seg__opt", extra?.className), "aria-pressed": o === value, disabled: disabled || extra?.disabled, title: extra?.title, onClick: () => onChange(o), children: label ? label(o) : o }, o));
|
|
100
|
+
}) }));
|
|
82
101
|
}
|
|
83
102
|
/* ── data ────────────────────────────────────────────────────────────── */
|
|
84
103
|
const STATUS_TONE = {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Where the dragged width is remembered, browser-side only (like the theme). */
|
|
2
|
+
export declare const INSPECTOR_W_KEY = "smoodly-inspector-w";
|
|
3
|
+
/** Below this the richtext toolbar and the outline rows no longer fit. */
|
|
4
|
+
export declare const MIN_INSPECTOR_W = 260;
|
|
5
|
+
/** The token's value in the warm look; what a reset returns to. */
|
|
6
|
+
export declare const DEFAULT_INSPECTOR_W = 320;
|
|
7
|
+
/** One arrow-key press. */
|
|
8
|
+
export declare const RESIZE_STEP = 16;
|
|
9
|
+
/** Half the editor row (canvas + handle + inspector), never below the minimum. */
|
|
10
|
+
export declare const maxInspectorW: (rowWidth: number) => number;
|
|
11
|
+
/** An integer inside [MIN, half the row]. */
|
|
12
|
+
export declare const clampInspectorW: (px: number, rowWidth: number) => number;
|
|
13
|
+
/** A stored value is trusted only as a plain integer ≥ MIN; anything else is
|
|
14
|
+
* ignored (never clamped or rewritten). The boot script in theme.tsx repeats
|
|
15
|
+
* this test inline — keep the two in step. */
|
|
16
|
+
export declare const parseStoredWidth: (raw: string | null) => number | null;
|