Craft: Excellence documentaire native

Pourquoi Craft a remporté l'Apple Design Award 2021 : performances natives, pages imbriquées, architecture en blocs et publication web en un clic. Avec patterns d'implémentation Swift.

5 min de lecture 978 mots
Craft: Excellence documentaire native screenshot

Craft : L'excellence documentaire native avant tout

« Nous croyons que les outils de réflexion devraient être une extension de vos pensées, pas un obstacle. »

Craft prouve que les applications natives peuvent offrir des expériences que les applications web ne peuvent égaler. Construite avec une intégration profonde de la plateforme, elle atteint la réactivité et le raffinement qui rendent la prise de notes numérique aussi naturelle que le papier.


Points clés

  1. Le natif surpasse Electron - Un temps de réponse inférieur à 50 ms nécessite une interface spécifique à la plateforme, pas des enveloppes web
  2. Des pages dans les pages - N'importe quel bloc peut devenir une page, permettant à la structure d'émerger du contenu
  3. Plusieurs dispositions, mêmes données - Listes, cartes, galeries et tableaux offrent la vue adaptée à chaque contexte
  4. Publication web en un clic - Les pages partagées fonctionnent sans compte destinataire ni téléchargement d'application
  5. Adapté à la plateforme, pas identique - Craft sur Mac ressemble à Mac ; Craft sur iOS ressemble à iOS

Pourquoi Craft est important

Craft a remporté l'Apple Design Award 2021 et obtient régulièrement la reconnaissance de l'App Store en refusant de compromettre les performances natives au profit de la commodité multiplateforme.

Réalisations clés : - Applications véritablement natives sur iOS, macOS, Windows (pas Electron) - Temps de réponse inférieur à 50 ms sur toutes les interactions - Conçu pour fonctionner hors ligne avec synchronisation transparente - Architecture basée sur les blocs sans la lenteur des applications web - De belles pages de partage qui fonctionnent sans compte


Philosophie de conception fondamentale

Natif d'abord, pas enveloppé dans du web

Le choix déterminant de Craft : construire des applications natives pour chaque plateforme, en utilisant une logique métier partagée mais une interface spécifique à la plateforme.

APPROCHE ELECTRON/WEB              APPROCHE NATIVE DE CRAFT
───────────────────────────────────────────────────────────────────
Base de code unique (JavaScript)   Interface spécifique (Swift, etc.)
Moteur de rendu web                Rendu natif
« Cohérence » multiplateforme      Comportement adapté à la plateforme
~200ms de latence d'entrée         ~16ms de latence d'entrée
Raccourcis clavier génériques      Raccourcis natifs de la plateforme
Sélection de texte web standard    Moteur de texte natif

Insight clé : Les utilisateurs ne veulent pas d'applications qui se ressemblent partout — ils veulent des applications qui semblent naturelles sur leur plateforme.

macOS via Catalyst (bien fait)

Craft utilise Mac Catalyst mais le personnalise intensivement pour qu'il semble véritablement natif sur Mac :

// Craft's Catalyst customization approach
#if targetEnvironment(macCatalyst)
extension SceneDelegate {
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession) {
        guard let windowScene = scene as? UIWindowScene else { return }

        // Enable native Mac toolbar
        if let titlebar = windowScene.titlebar {
            titlebar.titleVisibility = .hidden
            titlebar.toolbar = createNativeToolbar()
        }

        // Mac-specific window sizing
        windowScene.sizeRestrictions?.minimumSize = CGSize(width: 800, height: 600)

        // Enable window tabs
        UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .forEach { $0.titlebar?.toolbar?.showsBaselineSeparator = false }
    }

    private func createNativeToolbar() -> NSToolbar {
        let toolbar = NSToolbar(identifier: "CraftToolbar")
        toolbar.displayMode = .iconOnly
        toolbar.delegate = self
        return toolbar
    }
}
#endif

Bibliothèque de patterns

1. Architecture de contenu basée sur les blocs

Chaque élément dans Craft est un bloc. Chaque bloc avec du contenu est potentiellement une page, créant une imbrication infinie sans surcharge cognitive.

Types de blocs :

BLOCS DE TEXTE
├── Paragraphe (par défaut)
├── Titre 1, 2, 3
├── Citation
├── Encadré (info, avertissement, succès)
└── Code (avec coloration syntaxique)

BLOCS MÉDIA
├── Image (avec légende)
├── Vidéo
├── Pièce jointe
├── Dessin (Apple Pencil)
└── Enregistrement audio

BLOCS STRUCTURELS
├── Toggle (repliable)
├── Page (document imbriqué)
├── Séparateur
└── Tableau

Concept d'implémentation :

protocol CraftBlock: Identifiable {
    var id: UUID { get }
    var content: BlockContent { get set }
    var style: BlockStyle { get set }
    var children: [any CraftBlock] { get set }
    var metadata: BlockMetadata { get }
}

enum BlockContent {
    case text(AttributedString)
    case image(ImageData)
    case page(PageReference)
    case toggle(isExpanded: Bool)
    case code(language: String, content: String)
    case table(TableData)
}

struct BlockMetadata {
    let created: Date
    let modified: Date
    let createdBy: UserReference?
}

// Every block can contain other blocks
class PageBlock: CraftBlock {
    var id = UUID()
    var content: BlockContent
    var style: BlockStyle
    var children: [any CraftBlock] = []
    var metadata: BlockMetadata

    // A page is just a block that can be opened full-screen
    var canOpenAsPage: Bool { true }
}

Insight clé : Quand chaque bloc peut être une page, l'architecture de l'information émerge du contenu, pas d'une structure prédéterminée.


2. Pages imbriquées (pages dans les pages)

La fonctionnalité signature de Craft : n'importe quel bloc peut devenir une page, et les pages s'imbriquent à l'infini.

STRUCTURE DU DOCUMENT
───────────────────────────────────────────────────────────────────
📄 Projet Alpha
├── 📝 Paragraphe d'introduction
├── 📄 Notes de recherche       ← Ceci est une page (document imbriqué)
│   ├── 📝 Entretiens utilisateurs
│   ├── 📄 Entretien : Sarah    ← Une autre page imbriquée
│   │   └── 📝 Insights clés
│   └── 📝 Analyse concurrentielle
├── 📝 Aperçu du calendrier
└── 📄 Notes de réunion         ← Une autre page
    └── 📝 Actions à faire

Navigation : Le fil d'Ariane affiche le chemin
Projet Alpha > Notes de recherche > Entretien : Sarah

Pattern d'implémentation SwiftUI :

struct PageView: View {
    @Bindable var page: PageDocument
    @State private var selectedBlock: CraftBlock?

    var body: some View {
        ScrollView {
            LazyVStack(alignment: .leading, spacing: 0) {
                ForEach(page.blocks) { block in
                    BlockView(block: block, onTap: { tapped in
                        if tapped.canOpenAsPage {
                            navigateToPage(tapped)
                        } else {
                            selectedBlock = tapped
                        }
                    })
                }
            }
        }
        .navigationTitle(page.title)
        .toolbar {
            // Breadcrumb navigation
            ToolbarItem(placement: .principal) {
                BreadcrumbView(path: page.ancestorPath)
            }
        }
    }
}

struct BreadcrumbView: View {
    let path: [PageReference]

    var body: some View {
        HStack(spacing: 4) {
            ForEach(Array(path.enumerated()), id: \.element.id) { index, page in
                if index > 0 {
                    Image(systemName: "chevron.right")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
                Button(page.title) {
                    navigateTo(page)
                }
                .buttonStyle(.plain)
                .foregroundStyle(index == path.count - 1 ? .primary : .secondary)
            }
        }
    }
}

3. Système de mise en page par cartes

Craft propose 5 styles visuels pour les pages, rendant les documents lisibles en un coup d'œil.

Styles de cartes :

STYLE 1: LISTE (Par défaut)
┌────────────────────────────────────────┐
│ 📄 Titre de la page                    │
│    Texte d'aperçu affiché ici...       │
└────────────────────────────────────────┘

STYLE 2: CARTE (Moyenne)
┌──────────────────┐
│ ┌──────────────┐ │
│ │   [Image]    │ │
│ └──────────────┘ │
│ Titre de la page │
│ Texte d'aperçu...│
└──────────────────┘

STYLE 3: CARTE (Grande)
┌────────────────────────────────────────┐
│ ┌────────────────────────────────────┐ │
│ │                                    │ │
│ │        [Image de couverture]       │ │
│ │                                    │ │
│ └────────────────────────────────────┘ │
│ Titre de la page                       │
│ Texte d'aperçu plus long avec plus...  │
└────────────────────────────────────────┘

STYLE 4: GALERIE (Grille)
┌────────┐ ┌────────┐ ┌────────┐
│ [Img]  │ │ [Img]  │ │ [Img]  │
│ Titre  │ │ Titre  │ │ Titre  │
└────────┘ └────────┘ └────────┘

STYLE 5: TABLEAU (Style Kanban)
│ À faire  │ En cours │ Terminé  │
├──────────┼──────────┼──────────┤
│ Tâche 1  │ Tâche 3  │ Tâche 5  │
│ Tâche 2  │ Tâche 4  │          │

Implémentation :

enum PageLayoutStyle: String, CaseIterable {
    case list = "list"
    case cardMedium = "card_medium"
    case cardLarge = "card_large"
    case gallery = "gallery"
    case board = "board"
}

struct PageGridView: View {
    let pages: [PageReference]
    let style: PageLayoutStyle

    var body: some View {
        switch style {
        case .list:
            LazyVStack(spacing: 8) {
                ForEach(pages) { page in
                    ListRowView(page: page)
                }
            }

        case .cardMedium, .cardLarge:
            let columns = style == .cardMedium ? 3 : 2
            LazyVGrid(columns: Array(repeating: .init(.flexible()), count: columns)) {
                ForEach(pages) { page in
                    CardView(page: page, size: style == .cardLarge ? .large : .medium)
                }
            }

        case .gallery:
            LazyVGrid(columns: Array(repeating: .init(.flexible()), count: 4)) {
                ForEach(pages) { page in
                    GalleryThumbnail(page: page)
                }
            }

        case .board:
            BoardView(pages: pages)
        }
    }
}

4. Arrière-plans et thèmes de page

Chaque page peut avoir sa propre identité visuelle grâce aux arrière-plans et aux couleurs d'accentuation.

struct PageAppearance {
    // Background options
    enum Background {
        case solid(Color)
        case gradient(Gradient)
        case image(ImageReference)
        case paper(PaperTexture)
    }

    // Paper textures for writing feel
    enum PaperTexture: String {
        case none
        case lined
        case grid
        case dotted
    }

    var background: Background = .solid(.white)
    var accentColor: Color = .blue
    var iconEmoji: String?
    var coverImage: ImageReference?
}

// Applied to page
struct PageContainer: View {
    let page: PageDocument

    var body: some View {
        ZStack {
            // Background layer
            backgroundView(for: page.appearance.background)

            // Content layer
            PageContentView(page: page)
        }
    }

    @ViewBuilder
    private func backgroundView(for background: PageAppearance.Background) -> some View {
        switch background {
        case .solid(let color):
            color.ignoresSafeArea()
        case .gradient(let gradient):
            LinearGradient(gradient: gradient, startPoint: .top, endPoint: .bottom)
                .ignoresSafeArea()
        case .image(let ref):
            AsyncImage(url: ref.url) { image in
                image.resizable().scaledToFill()
            } placeholder: {
                Color.gray.opacity(0.1)
            }
            .ignoresSafeArea()
            .overlay(.ultraThinMaterial)
        case .paper(let texture):
            PaperTextureView(texture: texture)
        }
    }
}

5. Pages partagées (Publication web)

Craft génère de magnifiques pages web responsives à partir des documents. Aucun compte n'est requis pour les consulter.

DOCUMENT DANS CRAFT               PAGE PARTAGÉE SUR LE WEB
───────────────────────────────────────────────────────────────────
📄 Project Proposal               https://www.craft.do/s/abc123
├── 📝 Executive Summary    →
├── 📄 Budget Details             Mise en page épurée et responsive
├── 📝 Timeline                   Typographie préservée
└── 📄 Team Bios                  Images optimisées
                                  Mode sombre supporté
                                  Aucun compte Craft nécessaire

Fonctionnalités clés : - Publication en un clic - Domaines personnalisés disponibles - Option de protection par mot de passe - Statistiques de consultation - Rendu optimisé pour le SEO - Responsive sur tous les appareils


Système de design visuel

Palette de couleurs

extension Color {
    // Craft's signature palette
    static let craftPurple = Color(hex: "#6366F1")  // Primary accent
    static let craftBackground = Color(hex: "#FAFAFA")  // Light mode
    static let craftSurface = Color(hex: "#FFFFFF")

    // Semantic colors
    static let craftSuccess = Color(hex: "#10B981")
    static let craftWarning = Color(hex: "#F59E0B")
    static let craftError = Color(hex: "#EF4444")
    static let craftInfo = Color(hex: "#3B82F6")

    // Text hierarchy
    static let craftTextPrimary = Color(hex: "#111827")
    static let craftTextSecondary = Color(hex: "#6B7280")
    static let craftTextMuted = Color(hex: "#9CA3AF")
}

Typographie

struct CraftTypography {
    // Document typography
    static let title = Font.system(size: 32, weight: .bold, design: .default)
    static let heading1 = Font.system(size: 28, weight: .semibold)
    static let heading2 = Font.system(size: 22, weight: .semibold)
    static let heading3 = Font.system(size: 18, weight: .medium)
    static let body = Font.system(size: 16, weight: .regular)
    static let caption = Font.system(size: 14, weight: .regular)

    // Code blocks
    static let code = Font.system(size: 14, weight: .regular, design: .monospaced)

    // Line heights (generous for readability)
    static let bodyLineSpacing: CGFloat = 8
    static let headingLineSpacing: CGFloat = 4
}

Système d'espacement

struct CraftSpacing {
    // Block spacing
    static let blockGap: CGFloat = 4        // Between blocks
    static let sectionGap: CGFloat = 24     // Between sections
    static let pageMargin: CGFloat = 40     // Page edges (desktop)
    static let mobileMargin: CGFloat = 16   // Page edges (mobile)

    // Content width
    static let maxContentWidth: CGFloat = 720  // Optimal reading
    static let wideContentWidth: CGFloat = 960 // Tables, galleries
}

Philosophie de l'animation

Transitions de page fluides

// Navigation between pages uses matched geometry
struct NavigationTransition: View {
    @Namespace private var namespace
    @State private var selectedPage: PageReference?

    var body: some View {
        ZStack {
            // Page list
            PageListView(
                onSelect: { page in
                    withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
                        selectedPage = page
                    }
                },
                namespace: namespace
            )

            // Selected page expands from its card position
            if let page = selectedPage {
                PageDetailView(page: page)
                    .matchedGeometryEffect(id: page.id, in: namespace)
                    .transition(.scale.combined(with: .opacity))
            }
        }
    }
}

Animations des blocs

// Blocks animate when reordering
struct AnimatedBlockList: View {
    @Binding var blocks: [CraftBlock]

    var body: some View {
        LazyVStack(spacing: CraftSpacing.blockGap) {
            ForEach(blocks) { block in
                BlockView(block: block)
                    .transition(.asymmetric(
                        insertion: .move(edge: .top).combined(with: .opacity),
                        removal: .move(edge: .bottom).combined(with: .opacity)
                    ))
            }
        }
        .animation(.spring(response: 0.3, dampingFraction: 0.8), value: blocks)
    }
}

Enseignements pour notre travail

1. La performance native vaut l'investissement

Les utilisateurs ressentent la différence entre 16ms et 200ms de latence. Les apps natives l'emportent sur le ressenti.

2. Les pages dans les pages permettent une organisation organique

Laissez la structure émerger du contenu. N'obligez pas les utilisateurs à décider de la hiérarchie dès le départ.

3. Plusieurs dispositions visuelles pour le même contenu

Cartes, listes, galeries—mêmes données, différentes vues pour différents contextes.

4. Partagez sans friction

La publication web en un clic élimine entièrement le problème du « comment partager ça ? ».

5. Adapté à la plateforme, pas identique entre plateformes

Craft sur Mac donne l'impression d'une app Mac. Craft sur iOS donne l'impression d'une app iOS. C'est l'objectif.


Questions fréquemment posées

En quoi Craft est-il différent de Notion ?

Craft est natif (conçu spécifiquement pour les plateformes Apple), tandis que Notion est basé sur le web. Cela signifie que Craft atteint des temps de réponse inférieurs à 50ms, fonctionne entièrement hors ligne et s'intègre profondément aux fonctionnalités iOS/macOS comme Apple Pencil, Shortcuts et la recherche système. Notion offre plus de fonctionnalités de base de données ; Craft privilégie l'expérience d'écriture.

Puis-je utiliser Craft hors ligne ?

Oui. Craft stocke tous les documents localement et synchronise via iCloud lorsque connecté. Vous pouvez créer, éditer et organiser des documents sans internet. Les modifications se synchronisent automatiquement lorsque vous vous reconnectez.

Que se passe-t-il quand je partage une page Craft ?

Craft génère une page web responsive à une URL craft.do. Les destinataires la consultent dans n'importe quel navigateur sans créer de compte ni installer l'app. La page préserve la typographie, les images et la structure de pages imbriquées de votre document.

Craft supporte-t-il le Markdown ?

Craft utilise son propre format de blocs mais exporte en Markdown. Vous pouvez copier le contenu en Markdown ou exporter des documents entiers. Certains raccourcis Markdown fonctionnent pendant l'édition (comme # pour les titres), mais Craft privilégie l'édition visuelle plutôt que le balisage en texte brut.

Comment fonctionnent les pages imbriquées dans Craft ?

N'importe quel bloc de texte peut devenir une page en appuyant sur l'icône de page ou en tapant /page. Les pages imbriquées apparaissent en ligne dans le document parent et peuvent être ouvertes en plein écran. La navigation par fil d'Ariane indique votre position dans la hiérarchie. Cela crée une organisation naturelle sans imposer de structures de dossiers dès le départ.