@semiont/react-ui 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,8 @@ import { ReactElement } from 'react';
2
2
  import { RenderOptions, RenderResult } from '@testing-library/react';
3
3
  export * from '@testing-library/react';
4
4
  import { QueryClient } from '@tanstack/react-query';
5
- import { T as TranslationManager, S as SessionManager, O as OpenResourcesManager } from './EventBusContext-CLnb2LmB.mjs';
6
- export { r as resetEventBusForTesting } from './EventBusContext-CLnb2LmB.mjs';
5
+ import { T as TranslationManager, S as SessionManager, O as OpenResourcesManager } from './EventBusContext-DUIMowqQ.mjs';
6
+ export { r as resetEventBusForTesting } from './EventBusContext-DUIMowqQ.mjs';
7
7
  import { EventBus } from '@semiont/core';
8
8
  export { vi } from 'vitest';
9
9
  import 'react/jsx-runtime';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semiont/react-ui",
3
- "version": "0.3.7",
3
+ "version": "0.4.0",
4
4
  "description": "React components and hooks for Semiont",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.mts",
@@ -6,6 +6,7 @@ import { LOCALES } from '@semiont/api-client';
6
6
 
7
7
  export interface GenerationConfig {
8
8
  title: string;
9
+ storagePath: string;
9
10
  prompt?: string;
10
11
  language: string;
11
12
  temperature: number;
@@ -48,6 +49,7 @@ export function ConfigureGenerationStep({
48
49
  translations: t,
49
50
  }: ConfigureGenerationStepProps) {
50
51
  const [title, setTitle] = useState(defaultTitle);
52
+ const [storagePath, setStoragePath] = useState('');
51
53
  const [prompt, setPrompt] = useState('');
52
54
  const [language, setLanguage] = useState(locale);
53
55
  const [temperature, setTemperature] = useState(0.7);
@@ -58,6 +60,7 @@ export function ConfigureGenerationStep({
58
60
  const trimmedPrompt = prompt.trim();
59
61
  onGenerate({
60
62
  title,
63
+ storagePath: `file://${storagePath}`,
61
64
  ...(trimmedPrompt ? { prompt: trimmedPrompt } : {}),
62
65
  language,
63
66
  temperature,
@@ -84,6 +87,25 @@ export function ConfigureGenerationStep({
84
87
  />
85
88
  </div>
86
89
 
90
+ {/* Storage URI */}
91
+ <div className="semiont-form__field">
92
+ <label htmlFor="wizard-storagePath" className="semiont-form__label">
93
+ Save location
94
+ </label>
95
+ <div className="semiont-input-addon">
96
+ <span className="semiont-input-addon__prefix">file://</span>
97
+ <input
98
+ id="wizard-storagePath"
99
+ type="text"
100
+ value={storagePath}
101
+ onChange={(e) => setStoragePath(e.target.value)}
102
+ required
103
+ className="semiont-input semiont-input--addon"
104
+ placeholder="generated/my-resource.md"
105
+ />
106
+ </div>
107
+ </div>
108
+
87
109
  {/* Additional Instructions */}
88
110
  <div className="semiont-form__field">
89
111
  <label htmlFor="wizard-prompt" className="semiont-form__label">
@@ -46,6 +46,9 @@ export function SortableResourceTab({
46
46
 
47
47
  const iconEmoji = getResourceIcon(resource.mediaType);
48
48
  const isCurrentlyDragging = isSortableDragging || isDragging;
49
+ const tooltipText = resource.storageUri
50
+ ? resource.storageUri.replace(/^file:\/\//, '')
51
+ : resource.name;
49
52
 
50
53
  // Handle keyboard shortcuts for reordering (Alt + Up/Down)
51
54
  const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
@@ -76,7 +79,7 @@ export function SortableResourceTab({
76
79
  <LinkComponent
77
80
  href={href}
78
81
  className="semiont-resource-tab__link"
79
- title={resource.name}
82
+ title={tooltipText}
80
83
  >
81
84
  <span className="semiont-resource-tab__icon" aria-hidden="true">
82
85
  {iconEmoji}
@@ -324,6 +324,10 @@ describe('ResourceComposePage', () => {
324
324
  const nameInput = screen.getByLabelText('Resource Name');
325
325
  fireEvent.change(nameInput, { target: { value: 'Test Resource' } });
326
326
 
327
+ // Fill in save location
328
+ const storagePathInput = screen.getByLabelText('Save location');
329
+ fireEvent.change(storagePathInput, { target: { value: 'docs/test-resource.md' } });
330
+
327
331
  // Fill in content
328
332
  const editor = screen.getByTestId('code-editor');
329
333
  fireEvent.change(editor, { target: { value: 'Test content' } });
@@ -336,15 +340,11 @@ describe('ResourceComposePage', () => {
336
340
  expect(onSaveResource).toHaveBeenCalledWith({
337
341
  mode: 'new',
338
342
  name: 'Test Resource',
343
+ storageUri: 'file://docs/test-resource.md',
339
344
  content: 'Test content',
340
- file: undefined,
341
345
  format: 'text/markdown',
342
- charset: undefined,
343
346
  entityTypes: [],
344
347
  language: 'en',
345
- archiveOriginal: undefined,
346
- referenceId: undefined,
347
- sourceDocumentId: undefined,
348
348
  });
349
349
  });
350
350
  });
@@ -358,6 +358,10 @@ describe('ResourceComposePage', () => {
358
358
  const nameInput = screen.getByLabelText('Resource Name');
359
359
  fireEvent.change(nameInput, { target: { value: 'Test Resource' } });
360
360
 
361
+ // Fill in save location
362
+ const storagePathInput = screen.getByLabelText('Save location');
363
+ fireEvent.change(storagePathInput, { target: { value: 'docs/test-resource.md' } });
364
+
361
365
  // Select entity type
362
366
  const documentButton = screen.getByRole('button', { name: /Document entity type/ });
363
367
  fireEvent.click(documentButton);
@@ -402,6 +406,9 @@ describe('ResourceComposePage', () => {
402
406
  const nameInput = screen.getByLabelText('Resource Name');
403
407
  fireEvent.change(nameInput, { target: { value: 'Test Resource' } });
404
408
 
409
+ const storagePathInput = screen.getByLabelText('Save location');
410
+ fireEvent.change(storagePathInput, { target: { value: 'docs/test-resource.md' } });
411
+
405
412
  const submitButton = screen.getByRole('button', { name: 'Create Resource' });
406
413
  fireEvent.click(submitButton);
407
414
 
@@ -96,6 +96,7 @@ export interface ResourceComposePageProps {
96
96
  export interface SaveResourceParams {
97
97
  mode: 'new' | 'clone' | 'reference';
98
98
  name: string;
99
+ storageUri: string;
99
100
  content?: string;
100
101
  file?: File;
101
102
  format?: string;
@@ -150,6 +151,9 @@ export function ResourceComposePage({
150
151
  // Character encoding selection - default to UTF-8 (empty string means use default)
151
152
  const [selectedCharset, setSelectedCharset] = useState<string>('');
152
153
 
154
+ // Working-tree path (bare, no protocol prefix) — converted to file:// URI on submit
155
+ const [storagePath, setStoragePath] = useState('');
156
+
153
157
  // Archive original checkbox (for clones only)
154
158
  const [archiveOriginal, setArchiveOriginal] = useState(true);
155
159
 
@@ -216,6 +220,7 @@ export function ResourceComposePage({
216
220
  const params: SaveResourceParams = {
217
221
  mode,
218
222
  name: newResourceName,
223
+ storageUri: `file://${storagePath}`,
219
224
  content: newResourceContent,
220
225
  format: uploadedFile ? fileMimeType : selectedFormat,
221
226
  entityTypes: selectedEntityTypes,
@@ -352,6 +357,26 @@ export function ResourceComposePage({
352
357
  />
353
358
  </div>
354
359
 
360
+ {/* Storage URI */}
361
+ <div className="semiont-form__field">
362
+ <label htmlFor="storagePath" className="semiont-form__label">
363
+ Save location
364
+ </label>
365
+ <div className="semiont-input-addon">
366
+ <span className="semiont-input-addon__prefix">file://</span>
367
+ <input
368
+ id="storagePath"
369
+ type="text"
370
+ value={storagePath}
371
+ onChange={(e) => setStoragePath(e.target.value)}
372
+ placeholder="docs/my-resource.md"
373
+ required
374
+ className="semiont-input semiont-input--addon"
375
+ disabled={isCreating}
376
+ />
377
+ </div>
378
+ </div>
379
+
355
380
  {/* Entity Types Selection */}
356
381
  {(!isReferenceCompletion || selectedEntityTypes.length === 0) && (
357
382
  <div className="semiont-form__field semiont-form__entity-types">
@@ -195,6 +195,7 @@ export function ResourceViewerPage({
195
195
  const handleWizardGenerateSubmit = useCallback((referenceId: string, config: GenerationConfig) => {
196
196
  onGenerateDocument(referenceId, {
197
197
  title: config.title,
198
+ storageUri: config.storagePath,
198
199
  prompt: config.prompt,
199
200
  language: config.language,
200
201
  temperature: config.temperature,
@@ -253,7 +254,7 @@ export function ResourceViewerPage({
253
254
  useEffect(() => {
254
255
  if (resource && rUri) {
255
256
  const mediaType = getPrimaryMediaType(resource);
256
- addResource(rUri, resource.name, mediaType || undefined);
257
+ addResource(rUri, resource.name, mediaType || undefined, resource.storageUri);
257
258
  if (typeof localStorage !== 'undefined') {
258
259
  localStorage.setItem('lastViewedDocumentId', rUri);
259
260
  }
@@ -105,6 +105,72 @@
105
105
  font-size: var(--semiont-text-base);
106
106
  }
107
107
 
108
+ /* Input with inline prefix (e.g. "file://") */
109
+ .semiont-input-addon {
110
+ display: flex;
111
+ align-items: stretch;
112
+ border: 1px solid var(--semiont-color-gray-300);
113
+ border-radius: var(--semiont-radius-md);
114
+ overflow: hidden;
115
+ background-color: var(--semiont-bg-primary);
116
+ }
117
+
118
+ [data-theme="dark"] .semiont-input-addon {
119
+ border-color: var(--semiont-color-gray-700);
120
+ }
121
+
122
+ .semiont-input-addon:focus-within {
123
+ outline: 2px solid var(--semiont-color-primary-500);
124
+ outline-offset: -1px;
125
+ border-color: var(--semiont-color-primary-500);
126
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
127
+ }
128
+
129
+ [data-theme="dark"] .semiont-input-addon:focus-within {
130
+ outline-color: var(--semiont-color-primary-400);
131
+ border-color: var(--semiont-color-primary-400);
132
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
133
+ }
134
+
135
+ .semiont-input-addon__prefix {
136
+ display: flex;
137
+ align-items: center;
138
+ padding: 0.5rem 0.5rem 0.5rem 0.75rem;
139
+ font-size: var(--semiont-text-sm);
140
+ font-family: var(--semiont-font-mono, monospace);
141
+ color: var(--semiont-text-tertiary);
142
+ background-color: var(--semiont-color-gray-50);
143
+ border-right: 1px solid var(--semiont-color-gray-300);
144
+ user-select: none;
145
+ white-space: nowrap;
146
+ }
147
+
148
+ [data-theme="dark"] .semiont-input-addon__prefix {
149
+ background-color: var(--semiont-color-gray-800);
150
+ border-right-color: var(--semiont-color-gray-700);
151
+ color: var(--semiont-text-tertiary);
152
+ }
153
+
154
+ .semiont-input--addon {
155
+ border: none;
156
+ border-radius: 0;
157
+ outline: none;
158
+ box-shadow: none;
159
+ flex: 1;
160
+ min-width: 0;
161
+ }
162
+
163
+ [data-theme="dark"] .semiont-input--addon {
164
+ border: none;
165
+ }
166
+
167
+ .semiont-input--addon:focus-visible,
168
+ .semiont-input--addon:focus {
169
+ outline: none;
170
+ border: none;
171
+ box-shadow: none;
172
+ }
173
+
108
174
  /* Placeholder */
109
175
  .semiont-input::placeholder {
110
176
  color: var(--semiont-text-tertiary);