Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,9 +352,9 @@ Everything below is deferred; the app works without it. Areas are in rough prior
- **Settings split (D-12 follow-ups, suite 04 §4):** *Sign-in & security* lacks the Google
account link status / *Link Google account* (needs a server call listing the user's identity
providers) and *Sign out of all devices* (P2). *Offline & device data* shows only the current
app's device data, not the summary row per other app on this device. Write's *Page & export* defaults are stored but not yet
applied: new documents ignore the default layout / saved page setup and the export dialog
doesn't preselect the default format (as in v1). The `/settings/sheets` hand-over from
app's device data, not the summary row per other app on this device. Write's *Page & export* defaults are stored but not all
applied: new documents apply the default layout (T-1) but ignore the saved page setup, and the
export dialog doesn't preselect the default format (as in v1). The `/settings/sheets` hand-over from
Write and the `/settings/write` redirect were checked in the browser (D-18); the
*Settings of other apps* links were not clicked.
- **Shared chart packages (D-3 follow-ups, SL-27 in M10, Slides 08 §2.7, §2.10):** `ChartData`
Expand Down
20 changes: 14 additions & 6 deletions specs/02-design-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,12 +300,20 @@ tests). There is no mobile editing; v2 apps are the same Flutter web builds.
the frame the layout starts in, and `false` when it ends; the apps fold it into their view
mode, so no edit command is enabled whatever the role and View › Mode › Editing is off. It is
layout, not a change of the document: nothing is undoable or remembered, and widening the
window restores the mode the user had. Write: Viewing, no comment margin, and (T-12) 16 px
beside the text (`WriteDocumentScreen.mobileTextPadding` as `WonkyEditor.compactPadding`)
instead of the page margins — the page fills the width — or the pageless padding; the page
setup itself is unchanged. Sheets: the grid and sheet tabs (no formula bar or filter-view
bar), with (T-12) a 1 px line in the theme's `border` color between the title row and the
grid (`sheets.mobile.separator`). Slides: Slides 04 §16.1 and the presenter mode of
window restores the mode the user had. Write: Viewing, no comment margin, (T-1) always
Pageless (`WriteDocumentController.setReadOnlyView` sets the view's layout; the page setup
itself is unchanged), (T-12, T-1) 16 px beside, above and below the text
(`WriteDocumentScreen.mobileTextPadding` as `WonkyEditor.compactPadding`) instead of the
pageless padding, (T-1) a 1 px line in the theme's `border` color between the title row and
the page (`write.mobile.separator`), and (T-1) no text selection: the selections the user
makes are collapsed (`selectionGuard`, `WriteEditorController.selectable`), which a
read-only document doesn't show, so taps and swipes only scroll. Sheets: the grid and sheet
tabs (no formula bar or filter-view bar), with (T-12) a 1 px line in the theme's `border`
color between the title row and the grid (`sheets.mobile.separator`); (T-1) no cell
selection (every pointer pans the grid, taps and long presses do nothing, the selection
isn't painted) and a touch tab bar: 48 px tall with 42 px tabs in 15 px text, the all-sheets
menu (40 px) and the tabs only — no add button, tab menus (▾ or right-click), scroll arrows,
Explore or selection stats. Slides: Slides 04 §16.1 and the presenter mode of
Slides 10 §1.6.
- **Presenter bar** (`WonkyPresenterBar`, `wonky_ui`): the 52 px top bar of the mobile presenter
mode — close (*Exit presenter mode*), title with "3 of 12", *Previous slide* / *Next slide*
Expand Down
2 changes: 1 addition & 1 deletion specs/wonky_sheets/03-grid-and-editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ Undo restores the previous count.
|-------|----------|
| Mouse wheel | Vertical: 3 × default row height × zoom per notch (platform delta respected); Shift+wheel: horizontal |
| Trackpad | Pixel-precise 2D scrolling with platform momentum (`diagonalDragBehavior.free`) |
| Touch | `GridTouchTracker` (`grid/grid_touch.dart`): a tap selects like a click (double-tap edits); one finger past the touch slop scrolls, with a `ClampingScrollSimulation` fling on release; a long press (500 ms) then drag runs the grid's pointer drag (range selection, fill handle, drag-move, header resize/move, freeze bars) instead of scrolling; two fingers pinch-zoom about their midpoint (50–200 %, 5 % steps) and pan. Mouse, pen and trackpad input are unchanged |
| Touch | `GridTouchTracker` (`grid/grid_touch.dart`): a tap selects like a click (double-tap edits); one finger past the touch slop scrolls, with a `ClampingScrollSimulation` fling on release; a long press (500 ms) then drag runs the grid's pointer drag (range selection, fill handle, drag-move, header resize/move, freeze bars) instead of scrolling; two fingers pinch-zoom about their midpoint (50–200 %, 5 % steps) and pan. Mouse, pen and trackpad input are unchanged. In the read-only mobile layout (suite 02 §6.1, T-1) there is no cell selection: every pointer (a mouse in a narrow window too) pans like one finger, with the fling, taps and long presses do nothing, and the selection isn't painted (`SheetEditorController.readOnlyView`) |
| Mod+wheel / pinch (`PointerScaleEvent`, trackpad pinch on web) | Zoom about pointer (§2.3) |
| Pointer cancel | A `PointerCancelEvent` (the OS or browser took the pointer, a second finger during a long-press drag) aborts the drag in progress: no fill, move, resize, freeze or paint format is committed |
| Scrollbars | Thin auto-hide (`WonkyScrollArea` style); dragging the vertical thumb shows a tooltip "Row 12,345" |
Expand Down
7 changes: 7 additions & 0 deletions specs/wonky_sheets/04-screens-menus-toolbar.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,13 @@ Specified in [03 §9](03-grid-and-editing.md#9-formula-bar). Hidden via View ›
All sheet commands are one undo step (delete restores the sheet with all rows, cells and rules by
re-inserting the same ids).

**Mobile (T-1).** In the read-only mobile layout (suite 02 §6.1) the bar is 48 px tall
(`SheetTabBar.mobileHeight`) with 42 px tabs (`mobileTabHeight`) in 15 px text, and shows the
**≡** all-sheets menu (40 px) and the tabs only: no **+**, no tab menu (▾ or right-click), no
scroll arrows (the strip scrolls with a swipe), no Explore and no selection stats (there is no
cell selection, 03 §5). A tap activates a tab at once (no double-tap rename). The desktop bar is
unchanged.

---

## 7. Selection statistics and Explore
Expand Down
5 changes: 5 additions & 0 deletions specs/wonky_sheets/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ menus at the pointer, `RangeInput` picking while focused (also the pivot source
skeleton layer, automatic text color by fill contrast, sort undo/redo keeping the selection.
Phase 4: `RangeInput` in the filter views panel, side panel restore precedence (02 §4
`LocalViewState`), row display order for sorted filter views (03 §2.3, 07 §4.4).
Mobile (suite 02 §6.1, T-12, T-1; `test/features/sheet_editor/sheet_editor_screen_test.dart`):
the read-only layout has no cell selection (every pointer pans the grid, taps and long presses
do nothing, the selection isn't painted; 03 §5) and a touch tab bar (48 px, 42 px tabs, only
the all-sheets menu and the tabs; 04 §6). The panning was checked in tests only, not on a
phone.
Known gaps of the editor: no tile cache (direct painting; median 12.6–16 ms, p95 20–25 ms per
frame at 1M cells in the debug test VM on a loaded machine — decided by a release profile), bundled cell fonts (families other than the UI fonts fall
back), named-range dropdown in the name box, remote-edit toasts beyond "row deleted".
Expand Down
22 changes: 19 additions & 3 deletions specs/wonky_write/04-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -1608,7 +1608,12 @@ takes < 100 ms on a 100-page document.
document for the session (in-memory). The word being typed (caret inside or at its end) is
checked once the caret leaves it.
- **Rendering**: `SpellingAndGrammarStyler` style phase with `spellingErrorAttribution` (red
wavy underline in the `destructive` token). Presentation-only (§2.5).
wavy underline in the `destructive` token). Presentation-only (§2.5). When a flagged block is
removed (Backspace merging pasted empty paragraphs, a delete), its underlines are cleared in
a microtask, not from the document listener: that listener can run before `super_editor`'s
layout has seen the removal, and a style phase marked dirty then restyles the stale view
model, which still has the removed block. The layout threw ("No such position in document")
and stopped updating, so the pasted line breaks stayed visible and couldn't be deleted (#21).
- **Interactions**:
- Right-click a flagged word: the context-menu top section shows up to 5 suggestions
(`write.spell.replace {nodeId, range, word}`, one undo step), *Ignore*
Expand Down Expand Up @@ -1649,8 +1654,13 @@ fixes it in one undo step.
layout).

### 29.2 Layout modes
`pageSetup.layout`: `pages` (default for new documents, overridable in Settings › Write) or
`pageless`. Set with `write.doc.setLayout {layout}` (View › Page layout ▸; also in Page setup).
`pageSetup.layout`: `pageless` (default for new documents, T-1; overridable in Settings ›
Write › Page & export › Default layout) or `pages`. The server applies the default when it
creates a document (blank, built-in templates, the welcome document *New here? Read me first*,
Markdown and templates without a page setup; `DriveService.newDocumentPageSetup`: the locale
defaults of 07 §6.1 with the user's layout, `writeDefaults.pageless`, Pageless unless it is
`false`). Copies, imports and user templates keep their own page setup. Phones always show
Pageless (suite 02 §6.1), whatever the setting. Set with `write.doc.setLayout {layout}` (View › Page layout ▸; also in Page setup).
This is a meta change, undoable (`write.meta`). The page setup model is owned by
[07-import-export-print.md](07-import-export-print.md).

Expand Down Expand Up @@ -1698,6 +1708,12 @@ Pages are **computed, never stored**. Manual page breaks are not supported (01
stylesheet is memoized so the presenter is not recreated on every keystroke.
`PageChromeLayerBuilder` paints the page cards, headers, footers and page numbers in the
underlay, at offsets computed by the controller.
The page cards are painted under the document, so the phase also gives the last block
bottom padding (`tailPadding`): the rest of the last page below it plus the 48 px bottom
canvas padding (05 §16). The document's scroll extent then reaches the last page's bottom
edge, also when that page isn't full (#1). It is component padding, outside the measured
box, so it never feeds back into the measure; the document padding would not do, since
`super_editor` rebuilds its layout only when a component's view model changes.
4. **Oversized blocks (v1, P0)**: the page boundary inside the block is drawn as a **seam**.
The two page cards touch, there is a dashed separator line in the margin areas, and the
page number label sits in the gutter. Content is never hidden. **P1**:
Expand Down
9 changes: 5 additions & 4 deletions specs/wonky_write/05-screens-menus-toolbar.md
Original file line number Diff line number Diff line change
Expand Up @@ -663,8 +663,9 @@ export* = default layout (`write.prefs.setDefaultLayout {layout}`, key `pageless
page setup saved by *Page setup › Set as default* with *Reset* (`write.prefs.resetPageSetup`),
default export format (`write.prefs.setExportFormat {format}`: md, html, pdf, docx, odt, txt);
*Spelling* = *Ignore words in UPPERCASE* (`write.spell.toggleIgnoreUppercase`) and the personal
dictionary (remove = `write.prefs.removeDictionaryWord {word}`). New documents don't apply the
default layout/page setup yet and exports don't preselect the default format (TODO.md).
dictionary (remove = `write.prefs.removeDictionaryWord {word}`). The default layout is Pageless
when unset and new documents apply it (T-1, 04 §29.2); they don't apply the saved default page
setup yet and exports don't preselect the default format (TODO.md).

## 14. Empty, loading and error states

Expand Down Expand Up @@ -714,7 +715,7 @@ Switching modes is instant; announcements via `SemanticsService` ("Suggesting mo
| Document surface | page cards: `page` color, radius 2, light: shadow `0 1px 3px rgba(0,0,0,.12), 0 1px 2px rgba(0,0,0,.08)`; dark: 1 px `border` token, no shadow | full width |
| Page size | page setup (mm/in → logical px at 96 dpi) × zoom | n/a |
| Gap between pages | 16 px × zoom (min 8) | n/a |
| Canvas padding | 24 px top, 48 px bottom, horizontal centering; if viewport narrower than page: horizontal scroll unless *Fit width* | content column centered, 48 px top padding |
| Canvas padding | 24 px top, 48 px below the last page (the scroll extent always reaches it, also when the last page isn't full, 04 §29.3), horizontal centering; if viewport narrower than page: horizontal scroll unless *Fit width* | content column centered, 48 px top padding, 96 px below the last block (theme `pagelessPadding`) |
| Content width | page width − margins | `min(theme pageless max width, viewport − 96)` |
| Header/footer | painted in margins: theme `pageChrome` text style (default: 10 pt, `muted` equivalent of theme text), tokens expanded; double-click → Page setup › Header & footer | none |
| Page number indicator | on scroll, a floating pill "Page 3 of 7" near the scrollbar thumb for 1 s | none |
Expand All @@ -732,7 +733,7 @@ Breakpoints = shad `ShadBreakpoints` (sm 640, md 768, lg 1024, xl 1280, 2xl 1536
| lg–xl | full | prio ≥ 5 overflow | comment margin beside the page or overlaying the canvas's right edge | docked (squeezes canvas; Pages mode auto *Fit width* if the page no longer fits and user zoom was 100 %) |
| md–lg | star/move move into a ⋮ menu; avatars max 2 | prio ≥ 3 overflow; mode switcher icon-only | comment margin overlays the right edge | docked min width or overlay if canvas < 480 px |
| sm–md | title truncates; Share becomes icon button | prio ≥ 2 overflow | Pages rendered at fit-width zoom (indicator shows effective %); margin overlays (hidden below 600 px canvas) | overlay from the right over a scrim (380 px) |
| < sm (≥ 360) | app icon, title, ⋮, Share icon | Undo/Redo, style, B, I, link, list, ⋯ | Pageless-like padding 16 px (Pages still drawn as scaled cards); no margin | full-width overlay |
| < sm (≥ 360) | the read-only mobile layout (suite 02 §6.1): app icon, read-only title, sync status, *View only*, account menu, then a 1 px `border` line (`write.mobile.separator`, T-1) | none | always Pageless (T-1; the page setup is unchanged), 16 px beside, above and below the text, no text selection; no margin | none |

The word count bar (§11), when on, is the same at every width.

Expand Down
29 changes: 28 additions & 1 deletion wonky_server/lib/src/platform/drive/drive_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import 'dart:convert';

import 'package:serverpod/serverpod.dart';
import 'package:wonky_core/wonky_core.dart' show Role;
import 'package:wonky_write_core/wonky_write_core.dart' show Delta, DeltaUtils;
import 'package:wonky_write_core/wonky_write_core.dart'
show Delta, DeltaUtils, PageLayoutMode, PageSetup;

import '../../sheets/spaces/space_copier.dart';
import '../../slides/decks/slides_decks.dart';
Expand Down Expand Up @@ -30,6 +31,7 @@ import '../sharing/acl_events.dart';
import '../storage/content_sizes.dart';
import '../storage/storage_hooks.dart';
import '../storage/storage_service.dart';
import '../users/user_service.dart';
import 'drive_events.dart';
import 'drive_file_filters.dart';
import 'drive_listing.dart';
Expand Down Expand Up @@ -269,6 +271,21 @@ abstract final class DriveService {

/// Creates a document. [documentUuid] and [itemUuid] may be generated by
/// the client for offline creation; the call is idempotent on them.
/// The page setup JSON of a new document: the defaults for [language]
/// (`PageSetup.defaultsFor`, what the editor shows for `{}`) in the
/// user's default layout (`UserService.defaultPageless`).
static Future<String> newDocumentPageSetup(
Session session,
Actor actor, {
String? language,
}) async {
final pageless = await UserService.defaultPageless(session, actor.userId);
final setup = PageSetup.defaultsFor(language ?? 'en').toJson()
..['layout'] =
(pageless ? PageLayoutMode.pageless : PageLayoutMode.pages).name;
return jsonEncode(setup);
}

static Future<DriveItemView> createDocument(
Session session,
Actor actor, {
Expand Down Expand Up @@ -330,6 +347,16 @@ abstract final class DriveService {
} else if (initialMarkdown != null && initialMarkdown.isNotEmpty) {
content = WriteContent.fromMarkdown(initialMarkdown).delta;
}
// New documents (blank, built-in templates, the welcome document and
// templates without a page setup) open in the user's default layout:
// Pageless unless they chose Pages (Write 04 §29.2, T-1).
if (pageSetup == '{}') {
pageSetup = await newDocumentPageSetup(
session,
actor,
language: language,
);
}
// (v2) Quota (Drive 03 §7.2): blank documents are always allowed.
if (content != null) {
await StorageHooks.checkCaller(
Expand Down
11 changes: 11 additions & 0 deletions wonky_server/lib/src/platform/users/user_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,17 @@ abstract final class UserService {
return BuiltinThemeIds.wonkyDefault;
}

/// Whether the user's new documents are Pageless (Write settings › Page &
/// export › Default layout, `writeDefaults.pageless`; Write 05 §13.3).
/// Pageless unless the user chose Pages (T-1).
static Future<bool> defaultPageless(Session session, int userId) async {
final prefs = await UserPreferences.db.findFirstRow(
session,
where: (t) => t.userId.equals(userId),
);
return decodeObject(prefs?.writeDefaults)['pageless'] != false;
}

// --------------------------------------------------------------- search

/// The users related to `@me`: the owners of and grantees on the items
Expand Down
63 changes: 63 additions & 0 deletions wonky_server/test/integration/platform/drive_endpoint_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
library;

import 'dart:async';
import 'dart:convert';

import 'package:clock/clock.dart';
import 'package:serverpod/serverpod.dart';
import 'package:test/test.dart';
import 'package:wonky_write_core/wonky_write_core.dart' show PageSetup;
import 'package:wonky_server/src/platform/common/models.dart';
import 'package:wonky_server/src/platform/drive/drive_trash.dart';
import 'package:wonky_server/src/platform/drive/templates/builtin_templates.dart';
Expand Down Expand Up @@ -104,6 +106,67 @@ void main() {
},
);

group('when creating documents (T-1)', () {
Future<Map<String, dynamic>> pageSetupOf(DriveItemView doc) async {
final session = sessionBuilder.build();
final item = await DriveItem.db.findFirstRow(
session,
where: (t) => t.uuid.equals(doc.uuid),
);
final row = await Document.db.findFirstRow(
session,
where: (t) => t.itemId.equals(item!.id),
);
return (jsonDecode(row!.pageSetupJson) as Map).cast<String, dynamic>();
}

test('then a blank document is Pageless with the default page '
'setup', () async {
final doc = await TestDrive.document(endpoints, anna);

final setup = await pageSetupOf(doc);
expect(setup['layout'], 'pageless');
expect(
PageSetup.fromJson(setup),
PageSetup.fromJson(
PageSetup.defaultsFor('en').toJson()..['layout'] = 'pageless',
),
);
});

test('then documents from a built-in template or Markdown are '
'Pageless', () async {
final template = BuiltinTemplates.byKey('write.meeting-notes');
final fromTemplate = await endpoints.drive.createDocument(
anna.endpointSession,
null,
'Notes',
template.uuid,
null,
);
final fromMarkdown = await TestDrive.document(
endpoints,
anna,
markdown: '# Hi',
);

expect((await pageSetupOf(fromTemplate))['layout'], 'pageless');
expect((await pageSetupOf(fromMarkdown))['layout'], 'pageless');
});

test('then a user who chose Pages as the default layout gets '
'Pages', () async {
await endpoints.user.updatePreferences(
anna.endpointSession,
UserPreferencesPatch(writeDefaults: jsonEncode({'pageless': false})),
);

final doc = await TestDrive.document(endpoints, anna);

expect((await pageSetupOf(doc))['layout'], 'pages');
});
});

test(
'when creating from a built-in template then its content is used',
() async {
Expand Down
Loading
Loading